enzostvs HF Staff commited on
Commit
ec51066
·
1 Parent(s): 8e7d8ea

update multi pages

Browse files
app/api/ask-ai/route.ts CHANGED
@@ -10,10 +10,15 @@ import {
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
 
 
13
  REPLACE_END,
14
  SEARCH_START,
 
 
15
  } from "@/lib/prompts";
16
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
 
17
 
18
  const ipAddresses = new Map();
19
 
@@ -22,7 +27,7 @@ export async function POST(request: NextRequest) {
22
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
23
 
24
  const body = await request.json();
25
- const { prompt, provider, model, redesignMarkdown, html } = body;
26
 
27
  if (!model || (!prompt && !redesignMarkdown)) {
28
  return NextResponse.json(
@@ -34,6 +39,7 @@ export async function POST(request: NextRequest) {
34
  const selectedModel = MODELS.find(
35
  (m) => m.value === model || m.label === model
36
  );
 
37
  if (!selectedModel) {
38
  return NextResponse.json(
39
  { ok: false, error: "Invalid model selected" },
@@ -92,12 +98,10 @@ export async function POST(request: NextRequest) {
92
  : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
93
 
94
  try {
95
- // Create a stream response
96
  const encoder = new TextEncoder();
97
  const stream = new TransformStream();
98
  const writer = stream.writable.getWriter();
99
 
100
- // Start the response
101
  const response = new NextResponse(stream.readable, {
102
  headers: {
103
  "Content-Type": "text/plain; charset=utf-8",
@@ -107,7 +111,7 @@ export async function POST(request: NextRequest) {
107
  });
108
 
109
  (async () => {
110
- let completeResponse = "";
111
  try {
112
  const client = new InferenceClient(token);
113
  const chatCompletion = client.chatCompletionStream(
@@ -119,12 +123,14 @@ export async function POST(request: NextRequest) {
119
  role: "system",
120
  content: INITIAL_SYSTEM_PROMPT,
121
  },
 
 
 
 
122
  {
123
  role: "user",
124
  content: redesignMarkdown
125
  ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
126
- : html
127
- ? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.`
128
  : prompt,
129
  },
130
  ],
@@ -141,39 +147,7 @@ export async function POST(request: NextRequest) {
141
 
142
  const chunk = value.choices[0]?.delta?.content;
143
  if (chunk) {
144
- let newChunk = chunk;
145
- if (!selectedModel?.isThinker) {
146
- if (provider !== "sambanova") {
147
- await writer.write(encoder.encode(chunk));
148
- completeResponse += chunk;
149
-
150
- if (completeResponse.includes("</html>")) {
151
- break;
152
- }
153
- } else {
154
- if (chunk.includes("</html>")) {
155
- newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
156
- }
157
- completeResponse += newChunk;
158
- await writer.write(encoder.encode(newChunk));
159
- if (newChunk.includes("</html>")) {
160
- break;
161
- }
162
- }
163
- } else {
164
- const lastThinkTagIndex =
165
- completeResponse.lastIndexOf("</think>");
166
- completeResponse += newChunk;
167
- await writer.write(encoder.encode(newChunk));
168
- if (lastThinkTagIndex !== -1) {
169
- const afterLastThinkTag = completeResponse.slice(
170
- lastThinkTagIndex + "</think>".length
171
- );
172
- if (afterLastThinkTag.includes("</html>")) {
173
- break;
174
- }
175
- }
176
- }
177
  }
178
  }
179
  } catch (error: any) {
@@ -223,7 +197,7 @@ export async function PUT(request: NextRequest) {
223
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
224
 
225
  const body = await request.json();
226
- const { prompt, html, previousPrompt, provider, selectedElementHtml, model } =
227
  body;
228
 
229
  if (!prompt || !html) {
@@ -307,7 +281,7 @@ export async function PUT(request: NextRequest) {
307
  selectedElementHtml
308
  ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
309
  : ""
310
- }`,
311
  },
312
  {
313
  role: "user",
@@ -334,60 +308,170 @@ export async function PUT(request: NextRequest) {
334
  if (chunk) {
335
  const updatedLines: number[][] = [];
336
  let newHtml = html;
337
- let position = 0;
338
- let moreBlocks = true;
339
-
340
- while (moreBlocks) {
341
- const searchStartIndex = chunk.indexOf(SEARCH_START, position);
342
- if (searchStartIndex === -1) {
343
- moreBlocks = false;
344
- continue;
345
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
- const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
348
- if (dividerIndex === -1) {
349
- moreBlocks = false;
350
- continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }
 
352
 
353
- const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
354
- if (replaceEndIndex === -1) {
355
- moreBlocks = false;
356
- continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  }
 
358
 
359
- const searchBlock = chunk.substring(
360
- searchStartIndex + SEARCH_START.length,
361
- dividerIndex
362
- );
363
- const replaceBlock = chunk.substring(
364
- dividerIndex + DIVIDER.length,
365
- replaceEndIndex
366
- );
367
 
368
- if (searchBlock.trim() === "") {
369
- newHtml = `${replaceBlock}\n${newHtml}`;
370
- updatedLines.push([1, replaceBlock.split("\n").length]);
371
- } else {
372
- const blockPosition = newHtml.indexOf(searchBlock);
373
- if (blockPosition !== -1) {
374
- const beforeText = newHtml.substring(0, blockPosition);
375
- const startLineNumber = beforeText.split("\n").length;
376
- const replaceLines = replaceBlock.split("\n").length;
377
- const endLineNumber = startLineNumber + replaceLines - 1;
378
-
379
- updatedLines.push([startLineNumber, endLineNumber]);
380
- newHtml = newHtml.replace(searchBlock, replaceBlock);
381
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
 
384
- position = replaceEndIndex + REPLACE_END.length;
 
 
 
 
385
  }
386
 
387
  return NextResponse.json({
388
  ok: true,
389
- html: newHtml,
390
  updatedLines,
 
391
  });
392
  } else {
393
  return NextResponse.json(
 
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
13
+ NEW_PAGE_END,
14
+ NEW_PAGE_START,
15
  REPLACE_END,
16
  SEARCH_START,
17
+ UPDATE_PAGE_START,
18
+ UPDATE_PAGE_END,
19
  } from "@/lib/prompts";
20
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
21
+ import { Page } from "@/types";
22
 
23
  const ipAddresses = new Map();
24
 
 
27
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
28
 
29
  const body = await request.json();
30
+ const { prompt, provider, model, redesignMarkdown, previousPrompts, pages } = body;
31
 
32
  if (!model || (!prompt && !redesignMarkdown)) {
33
  return NextResponse.json(
 
39
  const selectedModel = MODELS.find(
40
  (m) => m.value === model || m.label === model
41
  );
42
+
43
  if (!selectedModel) {
44
  return NextResponse.json(
45
  { ok: false, error: "Invalid model selected" },
 
98
  : PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
99
 
100
  try {
 
101
  const encoder = new TextEncoder();
102
  const stream = new TransformStream();
103
  const writer = stream.writable.getWriter();
104
 
 
105
  const response = new NextResponse(stream.readable, {
106
  headers: {
107
  "Content-Type": "text/plain; charset=utf-8",
 
111
  });
112
 
113
  (async () => {
114
+ // let completeResponse = "";
115
  try {
116
  const client = new InferenceClient(token);
117
  const chatCompletion = client.chatCompletionStream(
 
123
  role: "system",
124
  content: INITIAL_SYSTEM_PROMPT,
125
  },
126
+ ...(pages?.length > 1 ? [{
127
+ role: "assistant",
128
+ content: `Here are the current pages:\n\n${pages.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}\n\nNow, please create a new page based on this code. Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
129
+ }] : []),
130
  {
131
  role: "user",
132
  content: redesignMarkdown
133
  ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
 
 
134
  : prompt,
135
  },
136
  ],
 
147
 
148
  const chunk = value.choices[0]?.delta?.content;
149
  if (chunk) {
150
+ await writer.write(encoder.encode(chunk));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
  }
153
  } catch (error: any) {
 
197
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
198
 
199
  const body = await request.json();
200
+ const { prompt, html, previousPrompt, provider, selectedElementHtml, model, pages } =
201
  body;
202
 
203
  if (!prompt || !html) {
 
281
  selectedElementHtml
282
  ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
283
  : ""
284
+ }. Also here are the current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}.`,
285
  },
286
  {
287
  role: "user",
 
308
  if (chunk) {
309
  const updatedLines: number[][] = [];
310
  let newHtml = html;
311
+ const updatedPages = [...(pages || [])];
312
+
313
+ 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');
314
+ let updatePageMatch;
315
+
316
+ while ((updatePageMatch = updatePageRegex.exec(chunk)) !== null) {
317
+ const [, pagePath, pageContent] = updatePageMatch;
318
+
319
+ const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
320
+ if (pageIndex !== -1) {
321
+ let pageHtml = updatedPages[pageIndex].html;
322
+
323
+ let processedContent = pageContent;
324
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
325
+ if (htmlMatch) {
326
+ processedContent = htmlMatch[1];
327
+ }
328
+ let position = 0;
329
+ let moreBlocks = true;
330
+
331
+ while (moreBlocks) {
332
+ const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
333
+ if (searchStartIndex === -1) {
334
+ moreBlocks = false;
335
+ continue;
336
+ }
337
+
338
+ const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
339
+ if (dividerIndex === -1) {
340
+ moreBlocks = false;
341
+ continue;
342
+ }
343
 
344
+ const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
345
+ if (replaceEndIndex === -1) {
346
+ moreBlocks = false;
347
+ continue;
348
+ }
349
+
350
+ const searchBlock = processedContent.substring(
351
+ searchStartIndex + SEARCH_START.length,
352
+ dividerIndex
353
+ );
354
+ const replaceBlock = processedContent.substring(
355
+ dividerIndex + DIVIDER.length,
356
+ replaceEndIndex
357
+ );
358
+
359
+ if (searchBlock.trim() === "") {
360
+ pageHtml = `${replaceBlock}\n${pageHtml}`;
361
+ updatedLines.push([1, replaceBlock.split("\n").length]);
362
+ } else {
363
+ const blockPosition = pageHtml.indexOf(searchBlock);
364
+ if (blockPosition !== -1) {
365
+ const beforeText = pageHtml.substring(0, blockPosition);
366
+ const startLineNumber = beforeText.split("\n").length;
367
+ const replaceLines = replaceBlock.split("\n").length;
368
+ const endLineNumber = startLineNumber + replaceLines - 1;
369
+
370
+ updatedLines.push([startLineNumber, endLineNumber]);
371
+ pageHtml = pageHtml.replace(searchBlock, replaceBlock);
372
+ }
373
+ }
374
+
375
+ position = replaceEndIndex + REPLACE_END.length;
376
+ }
377
+
378
+ updatedPages[pageIndex].html = pageHtml;
379
+
380
+ if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
381
+ newHtml = pageHtml;
382
+ }
383
  }
384
+ }
385
 
386
+ 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');
387
+ let newPageMatch;
388
+
389
+ while ((newPageMatch = newPageRegex.exec(chunk)) !== null) {
390
+ const [, pagePath, pageContent] = newPageMatch;
391
+
392
+ let pageHtml = pageContent;
393
+ const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
394
+ if (htmlMatch) {
395
+ pageHtml = htmlMatch[1];
396
+ }
397
+
398
+ const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
399
+
400
+ if (existingPageIndex !== -1) {
401
+ updatedPages[existingPageIndex] = {
402
+ path: pagePath,
403
+ html: pageHtml.trim()
404
+ };
405
+ } else {
406
+ updatedPages.push({
407
+ path: pagePath,
408
+ html: pageHtml.trim()
409
+ });
410
  }
411
+ }
412
 
413
+ if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_PAGE_START)) {
414
+ let position = 0;
415
+ let moreBlocks = true;
 
 
 
 
 
416
 
417
+ while (moreBlocks) {
418
+ const searchStartIndex = chunk.indexOf(SEARCH_START, position);
419
+ if (searchStartIndex === -1) {
420
+ moreBlocks = false;
421
+ continue;
 
 
 
 
 
 
 
 
422
  }
423
+
424
+ const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
425
+ if (dividerIndex === -1) {
426
+ moreBlocks = false;
427
+ continue;
428
+ }
429
+
430
+ const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
431
+ if (replaceEndIndex === -1) {
432
+ moreBlocks = false;
433
+ continue;
434
+ }
435
+
436
+ const searchBlock = chunk.substring(
437
+ searchStartIndex + SEARCH_START.length,
438
+ dividerIndex
439
+ );
440
+ const replaceBlock = chunk.substring(
441
+ dividerIndex + DIVIDER.length,
442
+ replaceEndIndex
443
+ );
444
+
445
+ if (searchBlock.trim() === "") {
446
+ newHtml = `${replaceBlock}\n${newHtml}`;
447
+ updatedLines.push([1, replaceBlock.split("\n").length]);
448
+ } else {
449
+ const blockPosition = newHtml.indexOf(searchBlock);
450
+ if (blockPosition !== -1) {
451
+ const beforeText = newHtml.substring(0, blockPosition);
452
+ const startLineNumber = beforeText.split("\n").length;
453
+ const replaceLines = replaceBlock.split("\n").length;
454
+ const endLineNumber = startLineNumber + replaceLines - 1;
455
+
456
+ updatedLines.push([startLineNumber, endLineNumber]);
457
+ newHtml = newHtml.replace(searchBlock, replaceBlock);
458
+ }
459
+ }
460
+
461
+ position = replaceEndIndex + REPLACE_END.length;
462
  }
463
 
464
+ // Update the main HTML if it's the index page
465
+ const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
466
+ if (mainPageIndex !== -1) {
467
+ updatedPages[mainPageIndex].html = newHtml;
468
+ }
469
  }
470
 
471
  return NextResponse.json({
472
  ok: true,
 
473
  updatedLines,
474
+ pages: updatedPages,
475
  });
476
  } else {
477
  return NextResponse.json(
app/api/me/projects/[namespace]/[repoId]/route.ts CHANGED
@@ -1,10 +1,10 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import Project from "@/models/Project";
6
  import dbConnect from "@/lib/mongodb";
7
- import { getPTag } from "@/lib/utils";
8
 
9
  export async function GET(
10
  req: NextRequest,
@@ -33,7 +33,6 @@ export async function GET(
33
  { status: 404 }
34
  );
35
  }
36
- const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
37
  try {
38
  const space = await spaceInfo({
39
  name: namespace + "/" + repoId,
@@ -60,25 +59,41 @@ export async function GET(
60
  );
61
  }
62
 
63
- const response = await fetch(space_url);
64
- if (!response.ok) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  return NextResponse.json(
66
  {
67
  ok: false,
68
- error: "Failed to fetch space HTML",
69
  },
70
  { status: 404 }
71
  );
72
  }
73
- let html = await response.text();
74
- // remove the last p tag including this url https://enzostvs-deepsite.hf.space
75
- html = html.replace(getPTag(namespace + "/" + repoId), "");
76
 
77
  return NextResponse.json(
78
  {
79
  project: {
80
  ...project,
81
- html,
82
  },
83
  ok: true,
84
  },
@@ -117,7 +132,7 @@ export async function PUT(
117
  await dbConnect();
118
  const param = await params;
119
  const { namespace, repoId } = param;
120
- const { html, prompts } = await req.json();
121
 
122
  const project = await Project.findOne({
123
  user_id: user.id,
@@ -138,11 +153,14 @@ export async function PUT(
138
  name: `${namespace}/${repoId}`,
139
  };
140
 
141
- const newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
142
- const file = new File([newHtml], "index.html", { type: "text/html" });
143
- await uploadFile({
 
 
 
144
  repo,
145
- file,
146
  accessToken: user.token as string,
147
  commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
148
  });
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { RepoDesignation, spaceInfo, uploadFiles, listFiles } from "@huggingface/hub";
3
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import Project from "@/models/Project";
6
  import dbConnect from "@/lib/mongodb";
7
+ import { Page } from "@/types";
8
 
9
  export async function GET(
10
  req: NextRequest,
 
33
  { status: 404 }
34
  );
35
  }
 
36
  try {
37
  const space = await spaceInfo({
38
  name: namespace + "/" + repoId,
 
59
  );
60
  }
61
 
62
+ const repo: RepoDesignation = {
63
+ type: "space",
64
+ name: `${namespace}/${repoId}`,
65
+ };
66
+
67
+ const htmlFiles: Page[] = [];
68
+
69
+ for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
70
+ if (fileInfo.path.endsWith(".html")) {
71
+ const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
72
+ if (res.ok) {
73
+ const html = await res.text();
74
+ htmlFiles.push({
75
+ path: fileInfo.path,
76
+ html,
77
+ });
78
+ }
79
+ }
80
+ }
81
+
82
+ if (htmlFiles.length === 0) {
83
  return NextResponse.json(
84
  {
85
  ok: false,
86
+ error: "No HTML files found",
87
  },
88
  { status: 404 }
89
  );
90
  }
 
 
 
91
 
92
  return NextResponse.json(
93
  {
94
  project: {
95
  ...project,
96
+ pages: htmlFiles,
97
  },
98
  ok: true,
99
  },
 
132
  await dbConnect();
133
  const param = await params;
134
  const { namespace, repoId } = param;
135
+ const { pages, prompts } = await req.json();
136
 
137
  const project = await Project.findOne({
138
  user_id: user.id,
 
153
  name: `${namespace}/${repoId}`,
154
  };
155
 
156
+ const files: File[] = [];
157
+ pages.forEach((page: Page) => {
158
+ const file = new File([page.html], page.path, { type: "text/html" });
159
+ files.push(file);
160
+ });
161
+ await uploadFiles({
162
  repo,
163
+ files,
164
  accessToken: user.token as string,
165
  commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
166
  });
app/api/me/projects/route.ts CHANGED
@@ -4,8 +4,9 @@ import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
4
  import { isAuthenticated } from "@/lib/auth";
5
  import Project from "@/models/Project";
6
  import dbConnect from "@/lib/mongodb";
7
- import { COLORS, getPTag } from "@/lib/utils";
8
- // import type user
 
9
  export async function GET() {
10
  const user = await isAuthenticated();
11
 
@@ -39,10 +40,6 @@ export async function GET() {
39
  );
40
  }
41
 
42
- /**
43
- * This API route creates a new project in Hugging Face Spaces.
44
- * It requires an Authorization header with a valid token and a JSON body with the project details.
45
- */
46
  export async function POST(request: NextRequest) {
47
  const user = await isAuthenticated();
48
 
@@ -50,9 +47,9 @@ export async function POST(request: NextRequest) {
50
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
51
  }
52
 
53
- const { title, html, prompts } = await request.json();
54
 
55
- if (!title || !html) {
56
  return NextResponse.json(
57
  { message: "Title and HTML content are required.", ok: false },
58
  { status: 400 }
@@ -63,7 +60,6 @@ export async function POST(request: NextRequest) {
63
 
64
  try {
65
  let readme = "";
66
- let newHtml = html;
67
 
68
  const newTitle = title
69
  .toLowerCase()
@@ -97,12 +93,14 @@ tags:
97
 
98
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
99
 
100
- newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
101
- const file = new File([newHtml], "index.html", { type: "text/html" });
102
  const readmeFile = new File([readme], "README.md", {
103
  type: "text/markdown",
104
  });
105
- const files = [file, readmeFile];
 
 
 
 
106
  await uploadFiles({
107
  repo,
108
  files,
 
4
  import { isAuthenticated } from "@/lib/auth";
5
  import Project from "@/models/Project";
6
  import dbConnect from "@/lib/mongodb";
7
+ import { COLORS } from "@/lib/utils";
8
+ import { Page } from "@/types";
9
+
10
  export async function GET() {
11
  const user = await isAuthenticated();
12
 
 
40
  );
41
  }
42
 
 
 
 
 
43
  export async function POST(request: NextRequest) {
44
  const user = await isAuthenticated();
45
 
 
47
  return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
48
  }
49
 
50
+ const { title, pages, prompts } = await request.json();
51
 
52
+ if (!title || !pages || pages.length === 0) {
53
  return NextResponse.json(
54
  { message: "Title and HTML content are required.", ok: false },
55
  { status: 400 }
 
60
 
61
  try {
62
  let readme = "";
 
63
 
64
  const newTitle = title
65
  .toLowerCase()
 
93
 
94
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
95
 
 
 
96
  const readmeFile = new File([readme], "README.md", {
97
  type: "text/markdown",
98
  });
99
+ const files = [readmeFile];
100
+ pages.forEach((page: Page) => {
101
+ const file = new File([page.html], page.path, { type: "text/html" });
102
+ files.push(file);
103
+ });
104
  await uploadFiles({
105
  repo,
106
  files,
app/layout.tsx CHANGED
@@ -82,6 +82,8 @@ async function getMe() {
82
  }
83
  }
84
 
 
 
85
  export default async function RootLayout({
86
  children,
87
  }: Readonly<{
 
82
  }
83
  }
84
 
85
+ // if domain isn't deepsite.hf.co or enzostvs-deepsite.hf.space redirect to deepsite.hf.co
86
+
87
  export default async function RootLayout({
88
  children,
89
  }: Readonly<{
app/projects/[namespace]/[repoId]/page.tsx CHANGED
@@ -32,9 +32,9 @@ export default async function ProjectNamespacePage({
32
  params: Promise<{ namespace: string; repoId: string }>;
33
  }) {
34
  const { namespace, repoId } = await params;
35
- const project = await getProject(namespace, repoId);
36
- if (!project?.html) {
37
  redirect("/projects");
38
  }
39
- return <AppEditor project={project} />;
40
  }
 
32
  params: Promise<{ namespace: string; repoId: string }>;
33
  }) {
34
  const { namespace, repoId } = await params;
35
+ const data = await getProject(namespace, repoId);
36
+ if (!data?.pages) {
37
  redirect("/projects");
38
  }
39
+ return <AppEditor project={data} pages={data.pages} />;
40
  }
app/projects/new/page.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { AppEditor } from "@/components/editor";
2
 
3
  export default function ProjectsNewPage() {
4
- return <AppEditor />;
5
  }
 
1
  import { AppEditor } from "@/components/editor";
2
 
3
  export default function ProjectsNewPage() {
4
+ return <AppEditor isNew />;
5
  }
components/editor/ask-ai/index.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { useState, useRef, useMemo } from "react";
4
  import classNames from "classnames";
5
  import { toast } from "sonner";
6
  import { useLocalStorage, useUpdateEffect } from "react-use";
@@ -10,7 +10,7 @@ import { FaStopCircle } from "react-icons/fa";
10
  import ProModal from "@/components/pro-modal";
11
  import { Button } from "@/components/ui/button";
12
  import { MODELS } from "@/lib/providers";
13
- import { HtmlHistory } from "@/types";
14
  import { InviteFriends } from "@/components/invite-friends";
15
  import { Settings } from "@/components/editor/ask-ai/settings";
16
  import { LoginModal } from "@/components/login-modal";
@@ -22,51 +22,75 @@ import { TooltipContent } from "@radix-ui/react-tooltip";
22
  import { SelectedHtmlElement } from "./selected-html-element";
23
  import { FollowUpTooltip } from "./follow-up-tooltip";
24
  import { isTheSameHtml } from "@/lib/compare-html-diff";
 
25
 
26
  export function AskAI({
27
- html,
28
- setHtml,
29
  onScrollToBottom,
30
  isAiWorking,
31
  setisAiWorking,
32
  isEditableModeEnabled = false,
 
 
33
  selectedElement,
34
  setSelectedElement,
35
  setIsEditableModeEnabled,
36
  onNewPrompt,
37
  onSuccess,
 
 
38
  }: {
39
- html: string;
40
- setHtml: (html: string) => void;
41
  onScrollToBottom: () => void;
 
42
  isAiWorking: boolean;
43
  onNewPrompt: (prompt: string) => void;
44
  htmlHistory?: HtmlHistory[];
45
  setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
46
- onSuccess: (h: string, p: string, n?: number[][]) => void;
 
47
  isEditableModeEnabled: boolean;
48
  setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
49
  selectedElement?: HTMLElement | null;
50
  setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
 
 
51
  }) {
52
  const refThink = useRef<HTMLDivElement | null>(null);
53
- const audio = useRef<HTMLAudioElement | null>(null);
54
 
55
  const [open, setOpen] = useState(false);
56
  const [prompt, setPrompt] = useState("");
57
- const [hasAsked, setHasAsked] = useState(false);
58
  const [previousPrompt, setPreviousPrompt] = useState("");
59
  const [provider, setProvider] = useLocalStorage("provider", "auto");
60
  const [model, setModel] = useLocalStorage("model", MODELS[0].value);
61
  const [openProvider, setOpenProvider] = useState(false);
62
  const [providerError, setProviderError] = useState("");
63
  const [openProModal, setOpenProModal] = useState(false);
64
- const [think, setThink] = useState<string | undefined>(undefined);
65
  const [openThink, setOpenThink] = useState(false);
66
  const [isThinking, setIsThinking] = useState(true);
67
- const [controller, setController] = useState<AbortController | null>(null);
68
  const [isFollowUp, setIsFollowUp] = useState(true);
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  const selectedModel = useMemo(() => {
71
  return MODELS.find((m: { value: string }) => m.value === model);
72
  }, [model]);
@@ -74,203 +98,93 @@ export function AskAI({
74
  const callAi = async (redesignMarkdown?: string) => {
75
  if (isAiWorking) return;
76
  if (!redesignMarkdown && !prompt.trim()) return;
77
- setisAiWorking(true);
78
- setProviderError("");
79
- setThink("");
80
- setOpenThink(false);
81
- setIsThinking(true);
82
-
83
- let contentResponse = "";
84
- let thinkResponse = "";
85
- let lastRenderTime = 0;
86
-
87
- const abortController = new AbortController();
88
- setController(abortController);
89
- try {
90
- onNewPrompt(prompt);
91
- if (isFollowUp && !redesignMarkdown && !isSameHtml) {
92
- const selectedElementHtml = selectedElement
93
- ? selectedElement.outerHTML
94
- : "";
95
- const request = await fetch("/api/ask-ai", {
96
- method: "PUT",
97
- body: JSON.stringify({
98
- prompt,
99
- provider,
100
- previousPrompt,
101
- model,
102
- html,
103
- selectedElementHtml,
104
- }),
105
- headers: {
106
- "Content-Type": "application/json",
107
- "x-forwarded-for": window.location.hostname,
108
- },
109
- signal: abortController.signal,
110
- });
111
- if (request && request.body) {
112
- const res = await request.json();
113
- if (!request.ok) {
114
- if (res.openLogin) {
115
- setOpen(true);
116
- } else if (res.openSelectProvider) {
117
- setOpenProvider(true);
118
- setProviderError(res.message);
119
- } else if (res.openProModal) {
120
- setOpenProModal(true);
121
- } else {
122
- toast.error(res.message);
123
- }
124
- setisAiWorking(false);
125
- return;
126
- }
127
- setHtml(res.html);
128
- toast.success("AI responded successfully");
129
- setPreviousPrompt(prompt);
130
- setPrompt("");
131
- setisAiWorking(false);
132
- onSuccess(res.html, prompt, res.updatedLines);
133
- if (audio.current) audio.current.play();
134
- }
135
- } else {
136
- const request = await fetch("/api/ask-ai", {
137
- method: "POST",
138
- body: JSON.stringify({
139
- prompt,
140
- provider,
141
- model,
142
- html: isSameHtml ? "" : html,
143
- redesignMarkdown,
144
- }),
145
- headers: {
146
- "Content-Type": "application/json",
147
- "x-forwarded-for": window.location.hostname,
148
- },
149
- signal: abortController.signal,
150
- });
151
- if (request && request.body) {
152
- const reader = request.body.getReader();
153
- const decoder = new TextDecoder("utf-8");
154
- const selectedModel = MODELS.find(
155
- (m: { value: string }) => m.value === model
156
- );
157
- let contentThink: string | undefined = undefined;
158
- const read = async () => {
159
- const { done, value } = await reader.read();
160
- if (done) {
161
- const isJson =
162
- contentResponse.trim().startsWith("{") &&
163
- contentResponse.trim().endsWith("}");
164
- const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
165
- if (jsonResponse && !jsonResponse.ok) {
166
- if (jsonResponse.openLogin) {
167
- setOpen(true);
168
- } else if (jsonResponse.openSelectProvider) {
169
- setOpenProvider(true);
170
- setProviderError(jsonResponse.message);
171
- } else if (jsonResponse.openProModal) {
172
- setOpenProModal(true);
173
- } else {
174
- toast.error(jsonResponse.message);
175
- }
176
- setisAiWorking(false);
177
- return;
178
- }
179
-
180
- toast.success("AI responded successfully");
181
- setPreviousPrompt(prompt);
182
- setPrompt("");
183
- setisAiWorking(false);
184
- setHasAsked(true);
185
- if (selectedModel?.isThinker) {
186
- setModel(MODELS[0].value);
187
- }
188
- if (audio.current) audio.current.play();
189
 
190
- // Now we have the complete HTML including </html>, so set it to be sure
191
- const finalDoc = contentResponse.match(
192
- /<!DOCTYPE html>[\s\S]*<\/html>/
193
- )?.[0];
194
- if (finalDoc) {
195
- setHtml(finalDoc);
196
- }
197
- onSuccess(finalDoc ?? contentResponse, prompt);
198
 
199
- return;
200
- }
 
 
 
201
 
202
- const chunk = decoder.decode(value, { stream: true });
203
- thinkResponse += chunk;
204
- if (selectedModel?.isThinker) {
205
- const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
206
- if (thinkMatch && !thinkResponse?.includes("</think>")) {
207
- if ((contentThink?.length ?? 0) < 3) {
208
- setOpenThink(true);
209
- }
210
- setThink(thinkMatch.replace("<think>", "").trim());
211
- contentThink += chunk;
212
- return read();
213
- }
214
- }
215
-
216
- contentResponse += chunk;
217
 
218
- const newHtml = contentResponse.match(
219
- /<!DOCTYPE html>[\s\S]*/
220
- )?.[0];
221
- if (newHtml) {
222
- setIsThinking(false);
223
- let partialDoc = newHtml;
224
- if (
225
- partialDoc.includes("<head>") &&
226
- !partialDoc.includes("</head>")
227
- ) {
228
- partialDoc += "\n</head>";
229
- }
230
- if (
231
- partialDoc.includes("<body") &&
232
- !partialDoc.includes("</body>")
233
- ) {
234
- partialDoc += "\n</body>";
235
- }
236
- if (!partialDoc.includes("</html>")) {
237
- partialDoc += "\n</html>";
238
- }
239
 
240
- // Throttle the re-renders to avoid flashing/flicker
241
- const now = Date.now();
242
- if (now - lastRenderTime > 300) {
243
- setHtml(partialDoc);
244
- lastRenderTime = now;
245
- }
 
 
 
 
 
 
 
246
 
247
- if (partialDoc.length > 200) {
248
- onScrollToBottom();
249
- }
250
- }
251
- read();
252
- };
253
 
254
- read();
 
 
 
 
255
  }
256
  }
257
- } catch (error: any) {
258
- setisAiWorking(false);
259
- toast.error(error.message);
260
- if (error.openLogin) {
261
- setOpen(true);
262
- }
263
  }
264
  };
265
 
266
- const stopController = () => {
267
- if (controller) {
268
- controller.abort();
269
- setController(null);
270
- setisAiWorking(false);
271
- setThink("");
272
- setOpenThink(false);
273
- setIsThinking(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
275
  };
276
 
@@ -287,8 +201,8 @@ export function AskAI({
287
  }, [isThinking]);
288
 
289
  const isSameHtml = useMemo(() => {
290
- return isTheSameHtml(html);
291
- }, [html]);
292
 
293
  return (
294
  <div className="px-3">
@@ -345,7 +259,8 @@ export function AskAI({
345
  <div className="flex items-center justify-start gap-2">
346
  <Loading overlay={false} className="!size-4" />
347
  <p className="text-neutral-400 text-sm">
348
- AI is {isThinking ? "thinking" : "coding"}...{" "}
 
349
  </p>
350
  </div>
351
  <div
@@ -357,11 +272,10 @@ export function AskAI({
357
  </div>
358
  </div>
359
  )}
360
- <input
361
- type="text"
362
  disabled={isAiWorking}
363
  className={classNames(
364
- "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
365
  {
366
  "!pt-2.5": selectedElement && !isAiWorking,
367
  }
@@ -369,7 +283,7 @@ export function AskAI({
369
  placeholder={
370
  selectedElement
371
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
372
- : hasAsked
373
  ? "Ask DeepSite for edits"
374
  : "Ask DeepSite anything..."
375
  }
@@ -434,9 +348,9 @@ export function AskAI({
434
  </Button>
435
  </div>
436
  </div>
437
- <LoginModal open={open} onClose={() => setOpen(false)} html={html} />
438
  <ProModal
439
- html={html}
440
  open={openProModal}
441
  onClose={() => setOpenProModal(false)}
442
  />
@@ -462,7 +376,7 @@ export function AskAI({
462
  </div>
463
  )}
464
  </div>
465
- <audio ref={audio} id="audio" className="hidden">
466
  <source src="/success.mp3" type="audio/mpeg" />
467
  Your browser does not support the audio element.
468
  </audio>
 
1
  "use client";
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { useState, useMemo, useRef } from "react";
4
  import classNames from "classnames";
5
  import { toast } from "sonner";
6
  import { useLocalStorage, useUpdateEffect } from "react-use";
 
10
  import ProModal from "@/components/pro-modal";
11
  import { Button } from "@/components/ui/button";
12
  import { MODELS } from "@/lib/providers";
13
+ import { HtmlHistory, Page } from "@/types";
14
  import { InviteFriends } from "@/components/invite-friends";
15
  import { Settings } from "@/components/editor/ask-ai/settings";
16
  import { LoginModal } from "@/components/login-modal";
 
22
  import { SelectedHtmlElement } from "./selected-html-element";
23
  import { FollowUpTooltip } from "./follow-up-tooltip";
24
  import { isTheSameHtml } from "@/lib/compare-html-diff";
25
+ import { useCallAi } from "@/hooks/useCallAi";
26
 
27
  export function AskAI({
28
+ currentPage,
29
+ previousPrompts,
30
  onScrollToBottom,
31
  isAiWorking,
32
  setisAiWorking,
33
  isEditableModeEnabled = false,
34
+ pages,
35
+ htmlHistory,
36
  selectedElement,
37
  setSelectedElement,
38
  setIsEditableModeEnabled,
39
  onNewPrompt,
40
  onSuccess,
41
+ setPages,
42
+ setCurrentPage,
43
  }: {
44
+ currentPage: Page;
45
+ pages: Page[];
46
  onScrollToBottom: () => void;
47
+ previousPrompts: string[];
48
  isAiWorking: boolean;
49
  onNewPrompt: (prompt: string) => void;
50
  htmlHistory?: HtmlHistory[];
51
  setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
52
+ isNew?: boolean;
53
+ onSuccess: (page: Page[], p: string, n?: number[][]) => void;
54
  isEditableModeEnabled: boolean;
55
  setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
56
  selectedElement?: HTMLElement | null;
57
  setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
58
+ setPages: React.Dispatch<React.SetStateAction<Page[]>>;
59
+ setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
60
  }) {
61
  const refThink = useRef<HTMLDivElement | null>(null);
 
62
 
63
  const [open, setOpen] = useState(false);
64
  const [prompt, setPrompt] = useState("");
 
65
  const [previousPrompt, setPreviousPrompt] = useState("");
66
  const [provider, setProvider] = useLocalStorage("provider", "auto");
67
  const [model, setModel] = useLocalStorage("model", MODELS[0].value);
68
  const [openProvider, setOpenProvider] = useState(false);
69
  const [providerError, setProviderError] = useState("");
70
  const [openProModal, setOpenProModal] = useState(false);
 
71
  const [openThink, setOpenThink] = useState(false);
72
  const [isThinking, setIsThinking] = useState(true);
73
+ const [think, setThink] = useState("");
74
  const [isFollowUp, setIsFollowUp] = useState(true);
75
 
76
+ const {
77
+ callAiNewProject,
78
+ callAiFollowUp,
79
+ callAiNewPage,
80
+ stopController,
81
+ audio: hookAudio,
82
+ } = useCallAi({
83
+ onNewPrompt,
84
+ onSuccess,
85
+ onScrollToBottom,
86
+ setPages,
87
+ setCurrentPage,
88
+ currentPage,
89
+ pages,
90
+ isAiWorking,
91
+ setisAiWorking,
92
+ });
93
+
94
  const selectedModel = useMemo(() => {
95
  return MODELS.find((m: { value: string }) => m.value === model);
96
  }, [model]);
 
98
  const callAi = async (redesignMarkdown?: string) => {
99
  if (isAiWorking) return;
100
  if (!redesignMarkdown && !prompt.trim()) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
+ if (isFollowUp && !redesignMarkdown && !isSameHtml) {
103
+ // Use follow-up function for existing projects
104
+ const selectedElementHtml = selectedElement
105
+ ? selectedElement.outerHTML
106
+ : "";
 
 
 
107
 
108
+ const result = await callAiFollowUp(
109
+ prompt,
110
+ previousPrompt,
111
+ selectedElementHtml
112
+ );
113
 
114
+ if (result?.error) {
115
+ handleError(result.error, result.message);
116
+ return;
117
+ }
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ if (result?.success) {
120
+ setPreviousPrompt(prompt);
121
+ setPrompt("");
122
+ }
123
+ } else if (isFollowUp && pages.length > 1 && isSameHtml) {
124
+ const result = await callAiNewPage(prompt, currentPage.path, [
125
+ ...(previousPrompts ?? []),
126
+ ...(htmlHistory?.map((h) => h.prompt) ?? []),
127
+ ]);
128
+ if (result?.error) {
129
+ handleError(result.error, result.message);
130
+ return;
131
+ }
 
 
 
 
 
 
 
 
132
 
133
+ if (result?.success) {
134
+ setPreviousPrompt(prompt);
135
+ setPrompt("");
136
+ }
137
+ } else {
138
+ const result = await callAiNewProject(
139
+ prompt,
140
+ redesignMarkdown,
141
+ handleThink,
142
+ () => {
143
+ setIsThinking(false);
144
+ }
145
+ );
146
 
147
+ if (result?.error) {
148
+ handleError(result.error, result.message);
149
+ return;
150
+ }
 
 
151
 
152
+ if (result?.success) {
153
+ setPreviousPrompt(prompt);
154
+ setPrompt("");
155
+ if (selectedModel?.isThinker) {
156
+ setModel(MODELS[0].value);
157
  }
158
  }
 
 
 
 
 
 
159
  }
160
  };
161
 
162
+ const handleThink = (think: string) => {
163
+ setThink(think);
164
+ setIsThinking(true);
165
+ setOpenThink(true);
166
+ };
167
+
168
+ const handleError = (error: string, message?: string) => {
169
+ switch (error) {
170
+ case "login_required":
171
+ setOpen(true);
172
+ break;
173
+ case "provider_required":
174
+ setOpenProvider(true);
175
+ setProviderError(message || "");
176
+ break;
177
+ case "pro_required":
178
+ setOpenProModal(true);
179
+ break;
180
+ case "api_error":
181
+ toast.error(message || "An error occurred");
182
+ break;
183
+ case "network_error":
184
+ toast.error(message || "Network error occurred");
185
+ break;
186
+ default:
187
+ toast.error("An unexpected error occurred");
188
  }
189
  };
190
 
 
201
  }, [isThinking]);
202
 
203
  const isSameHtml = useMemo(() => {
204
+ return isTheSameHtml(currentPage.html);
205
+ }, [currentPage.html]);
206
 
207
  return (
208
  <div className="px-3">
 
259
  <div className="flex items-center justify-start gap-2">
260
  <Loading overlay={false} className="!size-4" />
261
  <p className="text-neutral-400 text-sm">
262
+ {/* AI is {isThinking ? "thinking" : "coding"}...{" "} */}
263
+ AI is coding...
264
  </p>
265
  </div>
266
  <div
 
272
  </div>
273
  </div>
274
  )}
275
+ <textarea
 
276
  disabled={isAiWorking}
277
  className={classNames(
278
+ "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
279
  {
280
  "!pt-2.5": selectedElement && !isAiWorking,
281
  }
 
283
  placeholder={
284
  selectedElement
285
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
286
+ : isFollowUp && (!isSameHtml || pages?.length > 1)
287
  ? "Ask DeepSite for edits"
288
  : "Ask DeepSite anything..."
289
  }
 
348
  </Button>
349
  </div>
350
  </div>
351
+ <LoginModal open={open} onClose={() => setOpen(false)} pages={pages} />
352
  <ProModal
353
+ pages={pages}
354
  open={openProModal}
355
  onClose={() => setOpenProModal(false)}
356
  />
 
376
  </div>
377
  )}
378
  </div>
379
+ <audio ref={hookAudio} id="audio" className="hidden">
380
  <source src="/success.mp3" type="audio/mpeg" />
381
  Your browser does not support the audio element.
382
  </audio>
components/editor/ask-ai/settings.tsx CHANGED
@@ -80,16 +80,14 @@ export function Settings({
80
  </p>
81
  )}
82
  <label className="block">
83
- <p className="text-neutral-300 text-sm mb-2.5">
84
- Choose a DeepSeek model
85
- </p>
86
  <Select defaultValue={model} onValueChange={onModelChange}>
87
  <SelectTrigger className="w-full">
88
- <SelectValue placeholder="Select a DeepSeek model" />
89
  </SelectTrigger>
90
  <SelectContent>
91
  <SelectGroup>
92
- <SelectLabel>DeepSeek models</SelectLabel>
93
  {MODELS.map(
94
  ({
95
  value,
 
80
  </p>
81
  )}
82
  <label className="block">
83
+ <p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
 
 
84
  <Select defaultValue={model} onValueChange={onModelChange}>
85
  <SelectTrigger className="w-full">
86
+ <SelectValue placeholder="Select a model" />
87
  </SelectTrigger>
88
  <SelectContent>
89
  <SelectGroup>
90
+ <SelectLabel>Models</SelectLabel>
91
  {MODELS.map(
92
  ({
93
  value,
components/editor/deploy-button/index.tsx CHANGED
@@ -18,12 +18,13 @@ import { Input } from "@/components/ui/input";
18
  import { api } from "@/lib/api";
19
  import { LoginModal } from "@/components/login-modal";
20
  import { useUser } from "@/hooks/useUser";
 
21
 
22
  export function DeployButton({
23
- html,
24
  prompts,
25
  }: {
26
- html: string;
27
  prompts: string[];
28
  }) {
29
  const router = useRouter();
@@ -45,7 +46,7 @@ export function DeployButton({
45
  try {
46
  const res = await api.post("/me/projects", {
47
  title: config.title,
48
- html,
49
  prompts,
50
  });
51
  if (res.data.ok) {
@@ -60,8 +61,6 @@ export function DeployButton({
60
  }
61
  };
62
 
63
- // TODO add a way to do not allow people to deploy if the html is broken.
64
-
65
  return (
66
  <div className="flex items-center justify-end gap-5">
67
  <div className="relative flex items-center justify-end">
@@ -71,10 +70,10 @@ export function DeployButton({
71
  <div>
72
  <Button variant="default" className="max-lg:hidden !px-4">
73
  <MdSave className="size-4" />
74
- Deploy your Project
75
  </Button>
76
  <Button variant="default" size="sm" className="lg:hidden">
77
- Deploy
78
  </Button>
79
  </div>
80
  </PopoverTrigger>
@@ -99,11 +98,11 @@ export function DeployButton({
99
  </div>
100
  </div>
101
  <p className="text-xl font-semibold text-neutral-950">
102
- Deploy as Space!
103
  </p>
104
  <p className="text-sm text-neutral-500 mt-1.5">
105
- Save and Deploy your project to a Space on the Hub. Spaces are
106
- a way to share your project with the world.
107
  </p>
108
  </header>
109
  <main className="space-y-4 p-6">
@@ -123,7 +122,7 @@ export function DeployButton({
123
  </div>
124
  <div>
125
  <p className="text-sm text-neutral-700 mb-2">
126
- Then, let&apos;s deploy it!
127
  </p>
128
  <Button
129
  variant="black"
@@ -131,7 +130,7 @@ export function DeployButton({
131
  className="relative w-full"
132
  disabled={loading}
133
  >
134
- Deploy Space <Rocket className="size-4" />
135
  {loading && (
136
  <Loading className="ml-2 size-4 animate-spin" />
137
  )}
@@ -148,7 +147,7 @@ export function DeployButton({
148
  onClick={() => setOpen(true)}
149
  >
150
  <MdSave className="size-4" />
151
- Save your Project
152
  </Button>
153
  <Button
154
  variant="default"
@@ -156,16 +155,16 @@ export function DeployButton({
156
  className="lg:hidden"
157
  onClick={() => setOpen(true)}
158
  >
159
- Save
160
  </Button>
161
  </>
162
  )}
163
  <LoginModal
164
  open={open}
165
  onClose={() => setOpen(false)}
166
- html={html}
167
- title="Log In to save your Project"
168
- description="Log In through your Hugging Face account to save your project and increase your monthly free limit."
169
  />
170
  </div>
171
  </div>
 
18
  import { api } from "@/lib/api";
19
  import { LoginModal } from "@/components/login-modal";
20
  import { useUser } from "@/hooks/useUser";
21
+ import { Page } from "@/types";
22
 
23
  export function DeployButton({
24
+ pages,
25
  prompts,
26
  }: {
27
+ pages: Page[];
28
  prompts: string[];
29
  }) {
30
  const router = useRouter();
 
46
  try {
47
  const res = await api.post("/me/projects", {
48
  title: config.title,
49
+ pages,
50
  prompts,
51
  });
52
  if (res.data.ok) {
 
61
  }
62
  };
63
 
 
 
64
  return (
65
  <div className="flex items-center justify-end gap-5">
66
  <div className="relative flex items-center justify-end">
 
70
  <div>
71
  <Button variant="default" className="max-lg:hidden !px-4">
72
  <MdSave className="size-4" />
73
+ Publish your Project
74
  </Button>
75
  <Button variant="default" size="sm" className="lg:hidden">
76
+ Publish
77
  </Button>
78
  </div>
79
  </PopoverTrigger>
 
98
  </div>
99
  </div>
100
  <p className="text-xl font-semibold text-neutral-950">
101
+ Publish as Space!
102
  </p>
103
  <p className="text-sm text-neutral-500 mt-1.5">
104
+ Save and Publish your project to a Space on the Hub. Spaces
105
+ are a way to share your project with the world.
106
  </p>
107
  </header>
108
  <main className="space-y-4 p-6">
 
122
  </div>
123
  <div>
124
  <p className="text-sm text-neutral-700 mb-2">
125
+ Then, let&apos;s publish it!
126
  </p>
127
  <Button
128
  variant="black"
 
130
  className="relative w-full"
131
  disabled={loading}
132
  >
133
+ Publish Space <Rocket className="size-4" />
134
  {loading && (
135
  <Loading className="ml-2 size-4 animate-spin" />
136
  )}
 
147
  onClick={() => setOpen(true)}
148
  >
149
  <MdSave className="size-4" />
150
+ Publish your Project
151
  </Button>
152
  <Button
153
  variant="default"
 
155
  className="lg:hidden"
156
  onClick={() => setOpen(true)}
157
  >
158
+ Publish
159
  </Button>
160
  </>
161
  )}
162
  <LoginModal
163
  open={open}
164
  onClose={() => setOpen(false)}
165
+ pages={pages}
166
+ title="Log In to publish your Project"
167
+ description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
168
  />
169
  </div>
170
  </div>
components/editor/footer/index.tsx CHANGED
@@ -2,7 +2,7 @@ import classNames from "classnames";
2
  import { FaMobileAlt } from "react-icons/fa";
3
  import { HelpCircle, RefreshCcw, SparkleIcon } from "lucide-react";
4
  import { FaLaptopCode } from "react-icons/fa6";
5
- import { HtmlHistory } from "@/types";
6
  import { Button } from "@/components/ui/button";
7
  import { MdAdd } from "react-icons/md";
8
  import { History } from "@/components/editor/history";
@@ -23,7 +23,7 @@ const DEVICES = [
23
  export function Footer({
24
  onReset,
25
  htmlHistory,
26
- setHtml,
27
  device,
28
  setDevice,
29
  iframeRef,
@@ -31,7 +31,7 @@ export function Footer({
31
  onReset: () => void;
32
  htmlHistory?: HtmlHistory[];
33
  device: "desktop" | "mobile";
34
- setHtml: (html: string) => void;
35
  iframeRef?: React.RefObject<HTMLIFrameElement | null>;
36
  setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
37
  }) {
@@ -69,7 +69,7 @@ export function Footer({
69
  {htmlHistory && htmlHistory.length > 0 && (
70
  <>
71
  <p className="text-neutral-700">|</p>
72
- <History history={htmlHistory} setHtml={setHtml} />
73
  </>
74
  )}
75
  </div>
 
2
  import { FaMobileAlt } from "react-icons/fa";
3
  import { HelpCircle, RefreshCcw, SparkleIcon } from "lucide-react";
4
  import { FaLaptopCode } from "react-icons/fa6";
5
+ import { HtmlHistory, Page } from "@/types";
6
  import { Button } from "@/components/ui/button";
7
  import { MdAdd } from "react-icons/md";
8
  import { History } from "@/components/editor/history";
 
23
  export function Footer({
24
  onReset,
25
  htmlHistory,
26
+ setPages,
27
  device,
28
  setDevice,
29
  iframeRef,
 
31
  onReset: () => void;
32
  htmlHistory?: HtmlHistory[];
33
  device: "desktop" | "mobile";
34
+ setPages: (pages: Page[]) => void;
35
  iframeRef?: React.RefObject<HTMLIFrameElement | null>;
36
  setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
37
  }) {
 
69
  {htmlHistory && htmlHistory.length > 0 && (
70
  <>
71
  <p className="text-neutral-700">|</p>
72
+ <History history={htmlHistory} setPages={setPages} />
73
  </>
74
  )}
75
  </div>
components/editor/history/index.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { History as HistoryIcon } from "lucide-react";
2
- import { HtmlHistory } from "@/types";
3
  import {
4
  Popover,
5
  PopoverContent,
@@ -9,10 +9,10 @@ import { Button } from "@/components/ui/button";
9
 
10
  export function History({
11
  history,
12
- setHtml,
13
  }: {
14
  history: HtmlHistory[];
15
- setHtml: (html: string) => void;
16
  }) {
17
  return (
18
  <Popover>
@@ -57,7 +57,8 @@ export function History({
57
  variant="sky"
58
  size="xs"
59
  onClick={() => {
60
- setHtml(item.html);
 
61
  }}
62
  >
63
  Select
 
1
  import { History as HistoryIcon } from "lucide-react";
2
+ import { HtmlHistory, Page } from "@/types";
3
  import {
4
  Popover,
5
  PopoverContent,
 
9
 
10
  export function History({
11
  history,
12
+ setPages,
13
  }: {
14
  history: HtmlHistory[];
15
+ setPages: (pages: Page[]) => void;
16
  }) {
17
  return (
18
  <Popover>
 
57
  variant="sky"
58
  size="xs"
59
  onClick={() => {
60
+ console.log(item);
61
+ setPages(item.pages);
62
  }}
63
  >
64
  Select
components/editor/index.tsx CHANGED
@@ -1,5 +1,5 @@
1
  "use client";
2
- import { useRef, useState } from "react";
3
  import { toast } from "sonner";
4
  import { editor } from "monaco-editor";
5
  import Editor from "@monaco-editor/react";
@@ -22,16 +22,25 @@ import { Preview } from "@/components/editor/preview";
22
  import { useEditor } from "@/hooks/useEditor";
23
  import { AskAI } from "@/components/editor/ask-ai";
24
  import { DeployButton } from "./deploy-button";
25
- import { Project } from "@/types";
26
  import { SaveButton } from "./save-button";
27
  import { LoadProject } from "../my-projects/load-project";
28
  import { isTheSameHtml } from "@/lib/compare-html-diff";
 
29
 
30
- export const AppEditor = ({ project }: { project?: Project | null }) => {
31
- const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
 
 
 
 
 
 
 
 
32
  const [, copyToClipboard] = useCopyToClipboard();
33
- const { html, setHtml, htmlHistory, setHtmlHistory, prompts, setPrompts } =
34
- useEditor(project?.html ?? (htmlStorage as string) ?? defaultHTML);
35
  // get query params from URL
36
  const searchParams = useSearchParams();
37
  const router = useRouter();
@@ -46,6 +55,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
46
  const monacoRef = useRef<any>(null);
47
 
48
  const [currentTab, setCurrentTab] = useState("chat");
 
49
  const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
50
  const [isResizing, setIsResizing] = useState(false);
51
  const [isAiWorking, setIsAiWorking] = useState(false);
@@ -54,11 +64,6 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
54
  null
55
  );
56
 
57
- /**
58
- * Resets the layout based on screen size
59
- * - For desktop: Sets editor to 1/3 width and preview to 2/3
60
- * - For mobile: Removes inline styles to let CSS handle it
61
- */
62
  const resetLayout = () => {
63
  if (!editor.current || !preview.current) return;
64
 
@@ -78,10 +83,6 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
78
  }
79
  };
80
 
81
- /**
82
- * Handles resizing when the user drags the resizer
83
- * Ensures minimum widths are maintained for both panels
84
- */
85
  const handleResize = (e: MouseEvent) => {
86
  if (!editor.current || !preview.current || !resizer.current) return;
87
 
@@ -149,7 +150,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
149
 
150
  // Prevent accidental navigation away when AI is working or content has changed
151
  useEvent("beforeunload", (e) => {
152
- if (isAiWorking || !isTheSameHtml(html)) {
153
  e.preventDefault();
154
  return "";
155
  }
@@ -175,6 +176,15 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
175
  console.log("Editor validation markers:", markers);
176
  };
177
 
 
 
 
 
 
 
 
 
 
178
  return (
179
  <section className="h-[100dvh] bg-neutral-950 flex flex-col">
180
  <Header tab={currentTab} onNewTab={setCurrentTab}>
@@ -183,10 +193,11 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
183
  router.push(`/projects/${project.space_id}`);
184
  }}
185
  />
 
186
  {project?._id ? (
187
- <SaveButton html={html} prompts={prompts} />
188
  ) : (
189
- <DeployButton html={html} prompts={prompts} />
190
  )}
191
  </Header>
192
  <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full max-lg:h-[calc(100%-82px)] relative">
@@ -196,10 +207,43 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
196
  ref={editor}
197
  className="bg-neutral-900 relative flex-1 overflow-hidden h-full flex flex-col gap-2 pb-3"
198
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  <CopyIcon
200
- className="size-4 absolute top-2 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
201
  onClick={() => {
202
- copyToClipboard(html);
203
  toast.success("HTML copied to clipboard!");
204
  }}
205
  />
@@ -222,10 +266,17 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
222
  },
223
  wordWrap: "on",
224
  }}
225
- value={html}
226
  onChange={(value) => {
227
  const newValue = value ?? "";
228
- setHtml(newValue);
 
 
 
 
 
 
 
229
  }}
230
  onMount={(editor, monaco) => {
231
  editorRef.current = editor;
@@ -234,19 +285,13 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
234
  onValidate={handleEditorValidation}
235
  />
236
  <AskAI
237
- html={html}
238
- setHtml={(newHtml: string) => {
239
- setHtml(newHtml);
240
- }}
241
  htmlHistory={htmlHistory}
242
- onSuccess={(
243
- finalHtml: string,
244
- p: string,
245
- updatedLines?: number[][]
246
- ) => {
247
  const currentHistory = [...htmlHistory];
248
  currentHistory.unshift({
249
- html: finalHtml,
250
  createdAt: new Date(),
251
  prompt: p,
252
  });
@@ -277,6 +322,9 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
277
  }, 100);
278
  }
279
  }}
 
 
 
280
  isAiWorking={isAiWorking}
281
  setisAiWorking={setIsAiWorking}
282
  onNewPrompt={(prompt: string) => {
@@ -287,6 +335,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
287
  editorRef.current?.getModel()?.getLineCount() ?? 0
288
  );
289
  }}
 
290
  isEditableModeEnabled={isEditableModeEnabled}
291
  setIsEditableModeEnabled={setIsEditableModeEnabled}
292
  selectedElement={selectedElement}
@@ -300,11 +349,13 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
300
  </>
301
  )}
302
  <Preview
303
- html={html}
304
  isResizing={isResizing}
305
  isAiWorking={isAiWorking}
306
  ref={preview}
307
  device={device}
 
 
308
  currentTab={currentTab}
309
  isEditableModeEnabled={isEditableModeEnabled}
310
  iframeRef={iframeRef}
@@ -324,7 +375,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
324
  if (
325
  window.confirm("You're about to reset the editor. Are you sure?")
326
  ) {
327
- setHtml(defaultHTML);
328
  removeHtmlStorage();
329
  editorRef.current?.revealLine(
330
  editorRef.current?.getModel()?.getLineCount() ?? 0
@@ -332,7 +383,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
332
  }
333
  }}
334
  htmlHistory={htmlHistory}
335
- setHtml={setHtml}
336
  iframeRef={iframeRef}
337
  device={device}
338
  setDevice={setDevice}
 
1
  "use client";
2
+ import { useMemo, useRef, useState } from "react";
3
  import { toast } from "sonner";
4
  import { editor } from "monaco-editor";
5
  import Editor from "@monaco-editor/react";
 
22
  import { useEditor } from "@/hooks/useEditor";
23
  import { AskAI } from "@/components/editor/ask-ai";
24
  import { DeployButton } from "./deploy-button";
25
+ import { Page, Project } from "@/types";
26
  import { SaveButton } from "./save-button";
27
  import { LoadProject } from "../my-projects/load-project";
28
  import { isTheSameHtml } from "@/lib/compare-html-diff";
29
+ import { ListPages } from "./pages";
30
 
31
+ export const AppEditor = ({
32
+ project,
33
+ pages: initialPages,
34
+ isNew,
35
+ }: {
36
+ project?: Project | null;
37
+ pages?: Page[];
38
+ isNew?: boolean;
39
+ }) => {
40
+ const [htmlStorage, , removeHtmlStorage] = useLocalStorage("pages");
41
  const [, copyToClipboard] = useCopyToClipboard();
42
+ const { htmlHistory, setHtmlHistory, prompts, setPrompts, pages, setPages } =
43
+ useEditor(initialPages);
44
  // get query params from URL
45
  const searchParams = useSearchParams();
46
  const router = useRouter();
 
55
  const monacoRef = useRef<any>(null);
56
 
57
  const [currentTab, setCurrentTab] = useState("chat");
58
+ const [currentPage, setCurrentPage] = useState("index.html");
59
  const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
60
  const [isResizing, setIsResizing] = useState(false);
61
  const [isAiWorking, setIsAiWorking] = useState(false);
 
64
  null
65
  );
66
 
 
 
 
 
 
67
  const resetLayout = () => {
68
  if (!editor.current || !preview.current) return;
69
 
 
83
  }
84
  };
85
 
 
 
 
 
86
  const handleResize = (e: MouseEvent) => {
87
  if (!editor.current || !preview.current || !resizer.current) return;
88
 
 
150
 
151
  // Prevent accidental navigation away when AI is working or content has changed
152
  useEvent("beforeunload", (e) => {
153
+ if (isAiWorking || !isTheSameHtml(currentPageData?.html)) {
154
  e.preventDefault();
155
  return "";
156
  }
 
176
  console.log("Editor validation markers:", markers);
177
  };
178
 
179
+ const currentPageData = useMemo(() => {
180
+ return (
181
+ pages.find((page) => page.path === currentPage) ?? {
182
+ path: "index.html",
183
+ html: defaultHTML,
184
+ }
185
+ );
186
+ }, [pages, currentPage]);
187
+
188
  return (
189
  <section className="h-[100dvh] bg-neutral-950 flex flex-col">
190
  <Header tab={currentTab} onNewTab={setCurrentTab}>
 
193
  router.push(`/projects/${project.space_id}`);
194
  }}
195
  />
196
+ {/* for these buttons pass the whole pages */}
197
  {project?._id ? (
198
+ <SaveButton pages={pages} prompts={prompts} />
199
  ) : (
200
+ <DeployButton pages={pages} prompts={prompts} />
201
  )}
202
  </Header>
203
  <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full max-lg:h-[calc(100%-82px)] relative">
 
207
  ref={editor}
208
  className="bg-neutral-900 relative flex-1 overflow-hidden h-full flex flex-col gap-2 pb-3"
209
  >
210
+ <ListPages
211
+ pages={pages}
212
+ currentPage={currentPage}
213
+ onSelectPage={(path, newPath) => {
214
+ if (newPath) {
215
+ setPages((prev) =>
216
+ prev.map((page) =>
217
+ page.path === path ? { ...page, path: newPath } : page
218
+ )
219
+ );
220
+ setCurrentPage(newPath);
221
+ } else {
222
+ setCurrentPage(path);
223
+ }
224
+ }}
225
+ onDeletePage={(path) => {
226
+ const newPages = pages.filter((page) => page.path !== path);
227
+ setPages(newPages);
228
+ if (currentPage === path) {
229
+ setCurrentPage(newPages[0]?.path ?? "index.html");
230
+ }
231
+ }}
232
+ onNewPage={() => {
233
+ setPages((prev) => [
234
+ ...prev,
235
+ {
236
+ path: `page-${prev.length + 1}.html`,
237
+ html: defaultHTML,
238
+ },
239
+ ]);
240
+ setCurrentPage(`page-${pages.length + 1}.html`);
241
+ }}
242
+ />
243
  <CopyIcon
244
+ className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
245
  onClick={() => {
246
+ copyToClipboard(currentPageData.html);
247
  toast.success("HTML copied to clipboard!");
248
  }}
249
  />
 
266
  },
267
  wordWrap: "on",
268
  }}
269
+ value={currentPageData.html}
270
  onChange={(value) => {
271
  const newValue = value ?? "";
272
+ // setHtml(newValue);
273
+ setPages((prev) =>
274
+ prev.map((page) =>
275
+ page.path === currentPageData.path
276
+ ? { ...page, html: newValue }
277
+ : page
278
+ )
279
+ );
280
  }}
281
  onMount={(editor, monaco) => {
282
  editorRef.current = editor;
 
285
  onValidate={handleEditorValidation}
286
  />
287
  <AskAI
288
+ currentPage={currentPageData}
 
 
 
289
  htmlHistory={htmlHistory}
290
+ previousPrompts={project?.prompts ?? []}
291
+ onSuccess={(newPages, p: string, updatedLines?: number[][]) => {
 
 
 
292
  const currentHistory = [...htmlHistory];
293
  currentHistory.unshift({
294
+ pages: newPages,
295
  createdAt: new Date(),
296
  prompt: p,
297
  });
 
322
  }, 100);
323
  }
324
  }}
325
+ setPages={setPages}
326
+ pages={pages}
327
+ setCurrentPage={setCurrentPage}
328
  isAiWorking={isAiWorking}
329
  setisAiWorking={setIsAiWorking}
330
  onNewPrompt={(prompt: string) => {
 
335
  editorRef.current?.getModel()?.getLineCount() ?? 0
336
  );
337
  }}
338
+ isNew={isNew}
339
  isEditableModeEnabled={isEditableModeEnabled}
340
  setIsEditableModeEnabled={setIsEditableModeEnabled}
341
  selectedElement={selectedElement}
 
349
  </>
350
  )}
351
  <Preview
352
+ html={currentPageData?.html}
353
  isResizing={isResizing}
354
  isAiWorking={isAiWorking}
355
  ref={preview}
356
  device={device}
357
+ pages={pages}
358
+ setCurrentPage={setCurrentPage}
359
  currentTab={currentTab}
360
  isEditableModeEnabled={isEditableModeEnabled}
361
  iframeRef={iframeRef}
 
375
  if (
376
  window.confirm("You're about to reset the editor. Are you sure?")
377
  ) {
378
+ // setHtml(defaultHTML);
379
  removeHtmlStorage();
380
  editorRef.current?.revealLine(
381
  editorRef.current?.getModel()?.getLineCount() ?? 0
 
383
  }
384
  }}
385
  htmlHistory={htmlHistory}
386
+ setPages={setPages}
387
  iframeRef={iframeRef}
388
  device={device}
389
  setDevice={setDevice}
components/editor/pages/index.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Page } from "@/types";
2
+ import { PlusIcon } from "lucide-react";
3
+ import { ListPagesItem } from "./page";
4
+
5
+ export function ListPages({
6
+ pages,
7
+ currentPage,
8
+ onSelectPage,
9
+ onNewPage,
10
+ onDeletePage,
11
+ }: {
12
+ pages: Array<Page>;
13
+ currentPage: string;
14
+ onSelectPage: (path: string, newPath?: string) => void;
15
+ onNewPage: () => void;
16
+ onDeletePage: (path: string) => void;
17
+ }) {
18
+ return (
19
+ <div className="w-full flex items-center justify-start bg-neutral-950 overflow-auto flex-nowrap min-h-[44px]">
20
+ {pages.map((page, i) => (
21
+ <ListPagesItem
22
+ key={i}
23
+ page={page}
24
+ currentPage={currentPage}
25
+ onSelectPage={onSelectPage}
26
+ onDeletePage={onDeletePage}
27
+ index={i}
28
+ />
29
+ ))}
30
+ <button
31
+ className="max-h-14 min-h-14 pl-2 pr-4 py-4 text-neutral-400 cursor-pointer text-sm hover:bg-neutral-900 flex items-center justify-center gap-1 text-nowrap"
32
+ onClick={onNewPage}
33
+ >
34
+ <PlusIcon className="h-3" />
35
+ New Page
36
+ </button>
37
+ </div>
38
+ );
39
+ }
components/editor/pages/page.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { XIcon } from "lucide-react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import { Page } from "@/types";
6
+
7
+ export function ListPagesItem({
8
+ page,
9
+ currentPage,
10
+ onSelectPage,
11
+ onDeletePage,
12
+ index,
13
+ }: {
14
+ page: Page;
15
+ currentPage: string;
16
+ onSelectPage: (path: string, newPath?: string) => void;
17
+ onDeletePage: (path: string) => void;
18
+ index: number;
19
+ }) {
20
+ return (
21
+ <div
22
+ key={index}
23
+ className={classNames(
24
+ "pl-6 pr-1 py-3 text-neutral-400 cursor-pointer text-sm hover:bg-neutral-900 flex items-center justify-center gap-1 group text-nowrap border-r border-neutral-800",
25
+ {
26
+ "bg-neutral-900 !text-white": currentPage === page.path,
27
+ "!pr-6": index === 0, // Ensure the first item has padding on the right
28
+ }
29
+ )}
30
+ onClick={() => onSelectPage(page.path)}
31
+ title={page.path}
32
+ >
33
+ {/* {index > 0 && (
34
+ <Button
35
+ size="iconXsss"
36
+ variant="ghost"
37
+ onClick={(e) => {
38
+ e.stopPropagation();
39
+ // open the window modal to edit the name page
40
+ let newName = window.prompt(
41
+ "Enter new name for the page:",
42
+ page.path
43
+ );
44
+ if (newName && newName.trim() !== "") {
45
+ newName = newName.toLowerCase();
46
+ if (!newName.endsWith(".html")) {
47
+ newName = newName.replace(/\.[^/.]+$/, "");
48
+ newName = newName.replace(/\s+/g, "-");
49
+ newName += ".html";
50
+ }
51
+ onSelectPage(page.path, newName);
52
+ } else {
53
+ window.alert("Page name cannot be empty.");
54
+ }
55
+ }}
56
+ >
57
+ <EditIcon className="!h-3.5 text-neutral-400 cursor-pointer hover:text-neutral-300" />
58
+ </Button>
59
+ )} */}
60
+ {page.path}
61
+ {index > 0 && (
62
+ <Button
63
+ size="iconXsss"
64
+ variant="ghost"
65
+ className="group-hover:opacity-100 opacity-0"
66
+ onClick={(e) => {
67
+ e.stopPropagation();
68
+ if (
69
+ window.confirm(
70
+ "Are you sure you want to delete this page? This action cannot be undone."
71
+ )
72
+ ) {
73
+ onDeletePage(page.path);
74
+ }
75
+ }}
76
+ >
77
+ <XIcon className="h-3 text-neutral-400 cursor-pointer hover:text-neutral-300" />
78
+ </Button>
79
+ )}
80
+ </div>
81
+ );
82
+ }
components/editor/preview/index.tsx CHANGED
@@ -7,6 +7,7 @@ import { toast } from "sonner";
7
  import { cn } from "@/lib/utils";
8
  import { GridPattern } from "@/components/magic-ui/grid-pattern";
9
  import { htmlTagToText } from "@/lib/html-tag-to-text";
 
10
 
11
  export const Preview = ({
12
  html,
@@ -16,12 +17,16 @@ export const Preview = ({
16
  device,
17
  currentTab,
18
  iframeRef,
 
 
19
  isEditableModeEnabled,
20
  onClickElement,
21
  }: {
22
  html: string;
23
  isResizing: boolean;
24
  isAiWorking: boolean;
 
 
25
  ref: React.RefObject<HTMLDivElement | null>;
26
  iframeRef?: React.RefObject<HTMLIFrameElement | null>;
27
  device: "desktop" | "mobile";
@@ -65,6 +70,49 @@ export const Preview = ({
65
  }
66
  }
67
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  useUpdateEffect(() => {
70
  const cleanupListeners = () => {
@@ -79,7 +127,6 @@ export const Preview = ({
79
  if (iframeRef?.current) {
80
  const iframeDocument = iframeRef.current.contentDocument;
81
  if (iframeDocument) {
82
- // Clean up existing listeners first
83
  cleanupListeners();
84
 
85
  if (isEditableModeEnabled) {
@@ -90,7 +137,6 @@ export const Preview = ({
90
  }
91
  }
92
 
93
- // Clean up when component unmounts or dependencies change
94
  return cleanupListeners;
95
  }, [iframeRef, isEditableModeEnabled]);
96
 
@@ -169,6 +215,14 @@ export const Preview = ({
169
  behavior: isAiWorking ? "instant" : "smooth",
170
  });
171
  }
 
 
 
 
 
 
 
 
172
  }}
173
  />
174
  </div>
 
7
  import { cn } from "@/lib/utils";
8
  import { GridPattern } from "@/components/magic-ui/grid-pattern";
9
  import { htmlTagToText } from "@/lib/html-tag-to-text";
10
+ import { Page } from "@/types";
11
 
12
  export const Preview = ({
13
  html,
 
17
  device,
18
  currentTab,
19
  iframeRef,
20
+ pages,
21
+ setCurrentPage,
22
  isEditableModeEnabled,
23
  onClickElement,
24
  }: {
25
  html: string;
26
  isResizing: boolean;
27
  isAiWorking: boolean;
28
+ pages: Page[];
29
+ setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
30
  ref: React.RefObject<HTMLDivElement | null>;
31
  iframeRef?: React.RefObject<HTMLIFrameElement | null>;
32
  device: "desktop" | "mobile";
 
70
  }
71
  }
72
  };
73
+ const handleCustomNavigation = (event: MouseEvent) => {
74
+ if (iframeRef?.current) {
75
+ const iframeDocument = iframeRef.current.contentDocument;
76
+ if (iframeDocument) {
77
+ const findClosestAnchor = (
78
+ element: HTMLElement
79
+ ): HTMLAnchorElement | null => {
80
+ let current = element;
81
+ while (current && current !== iframeDocument.body) {
82
+ if (current.tagName === "A") {
83
+ return current as HTMLAnchorElement;
84
+ }
85
+ current = current.parentElement as HTMLElement;
86
+ }
87
+ return null;
88
+ };
89
+
90
+ const anchorElement = findClosestAnchor(event.target as HTMLElement);
91
+
92
+ if (anchorElement) {
93
+ let href = anchorElement.getAttribute("href");
94
+ if (href) {
95
+ event.stopPropagation();
96
+ event.preventDefault();
97
+
98
+ if (href.includes("#") && !href.includes(".html")) {
99
+ const targetElement = iframeDocument.querySelector(href);
100
+ if (targetElement) {
101
+ targetElement.scrollIntoView({ behavior: "smooth" });
102
+ }
103
+ return;
104
+ }
105
+
106
+ href = href.split(".html")[0] + ".html";
107
+ const isPageExist = pages.some((page) => page.path === href);
108
+ if (isPageExist) {
109
+ setCurrentPage(href);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ };
116
 
117
  useUpdateEffect(() => {
118
  const cleanupListeners = () => {
 
127
  if (iframeRef?.current) {
128
  const iframeDocument = iframeRef.current.contentDocument;
129
  if (iframeDocument) {
 
130
  cleanupListeners();
131
 
132
  if (isEditableModeEnabled) {
 
137
  }
138
  }
139
 
 
140
  return cleanupListeners;
141
  }, [iframeRef, isEditableModeEnabled]);
142
 
 
215
  behavior: isAiWorking ? "instant" : "smooth",
216
  });
217
  }
218
+ // add event listener to all links in the iframe to handle navigation
219
+ if (iframeRef?.current?.contentWindow?.document) {
220
+ const links =
221
+ iframeRef.current.contentWindow.document.querySelectorAll("a");
222
+ links.forEach((link) => {
223
+ link.addEventListener("click", handleCustomNavigation);
224
+ });
225
+ }
226
  }}
227
  />
228
  </div>
components/editor/save-button/index.tsx CHANGED
@@ -7,12 +7,13 @@ import { useParams } from "next/navigation";
7
  import Loading from "@/components/loading";
8
  import { Button } from "@/components/ui/button";
9
  import { api } from "@/lib/api";
 
10
 
11
  export function SaveButton({
12
- html,
13
  prompts,
14
  }: {
15
- html: string;
16
  prompts: string[];
17
  }) {
18
  // get params from URL
@@ -27,7 +28,7 @@ export function SaveButton({
27
 
28
  try {
29
  const res = await api.put(`/me/projects/${namespace}/${repoId}`, {
30
- html,
31
  prompts,
32
  });
33
  if (res.data.ok) {
@@ -59,7 +60,7 @@ export function SaveButton({
59
  onClick={updateSpace}
60
  >
61
  <MdSave className="size-4" />
62
- Deploy your Project{" "}
63
  {loading && <Loading className="ml-2 size-4 animate-spin" />}
64
  </Button>
65
  <Button
@@ -68,7 +69,7 @@ export function SaveButton({
68
  className="lg:hidden relative"
69
  onClick={updateSpace}
70
  >
71
- Deploy {loading && <Loading className="ml-2 size-4 animate-spin" />}
72
  </Button>
73
  </>
74
  );
 
7
  import Loading from "@/components/loading";
8
  import { Button } from "@/components/ui/button";
9
  import { api } from "@/lib/api";
10
+ import { Page } from "@/types";
11
 
12
  export function SaveButton({
13
+ pages,
14
  prompts,
15
  }: {
16
+ pages: Page[];
17
  prompts: string[];
18
  }) {
19
  // get params from URL
 
28
 
29
  try {
30
  const res = await api.put(`/me/projects/${namespace}/${repoId}`, {
31
+ pages,
32
  prompts,
33
  });
34
  if (res.data.ok) {
 
60
  onClick={updateSpace}
61
  >
62
  <MdSave className="size-4" />
63
+ Publish your Project{" "}
64
  {loading && <Loading className="ml-2 size-4 animate-spin" />}
65
  </Button>
66
  <Button
 
69
  className="lg:hidden relative"
70
  onClick={updateSpace}
71
  >
72
+ Publish {loading && <Loading className="ml-2 size-4 animate-spin" />}
73
  </Button>
74
  </>
75
  );
components/login-modal/index.tsx CHANGED
@@ -3,25 +3,26 @@ import { Button } from "@/components/ui/button";
3
  import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
4
  import { useUser } from "@/hooks/useUser";
5
  import { isTheSameHtml } from "@/lib/compare-html-diff";
 
6
 
7
  export const LoginModal = ({
8
  open,
9
- html,
10
  onClose,
11
  title = "Log In to use DeepSite for free",
12
  description = "Log In through your Hugging Face account to continue using DeepSite and increase your monthly free limit.",
13
  }: {
14
  open: boolean;
15
- html?: string;
16
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
17
  title?: string;
18
  description?: string;
19
  }) => {
20
  const { openLoginWindow } = useUser();
21
- const [, setStorage] = useLocalStorage("html_content");
22
  const handleClick = async () => {
23
- if (html && !isTheSameHtml(html)) {
24
- setStorage(html);
25
  }
26
  openLoginWindow();
27
  onClose(false);
 
3
  import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
4
  import { useUser } from "@/hooks/useUser";
5
  import { isTheSameHtml } from "@/lib/compare-html-diff";
6
+ import { Page } from "@/types";
7
 
8
  export const LoginModal = ({
9
  open,
10
+ pages,
11
  onClose,
12
  title = "Log In to use DeepSite for free",
13
  description = "Log In through your Hugging Face account to continue using DeepSite and increase your monthly free limit.",
14
  }: {
15
  open: boolean;
16
+ pages?: Page[];
17
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
18
  title?: string;
19
  description?: string;
20
  }) => {
21
  const { openLoginWindow } = useUser();
22
+ const [, setStorage] = useLocalStorage("pages");
23
  const handleClick = async () => {
24
+ if (pages && !isTheSameHtml(pages[0].html)) {
25
+ setStorage(pages);
26
  }
27
  openLoginWindow();
28
  onClose(false);
components/pro-modal/index.tsx CHANGED
@@ -3,20 +3,21 @@ import { Button } from "@/components/ui/button";
3
  import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
4
  import { CheckCheck } from "lucide-react";
5
  import { isTheSameHtml } from "@/lib/compare-html-diff";
 
6
 
7
  export const ProModal = ({
8
  open,
9
- html,
10
  onClose,
11
  }: {
12
  open: boolean;
13
- html: string;
14
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
15
  }) => {
16
- const [, setStorage] = useLocalStorage("html_content");
17
  const handleProClick = () => {
18
- if (!isTheSameHtml(html)) {
19
- setStorage(html);
20
  }
21
  window.open("https://huggingface.co/subscribe/pro?from=DeepSite", "_blank");
22
  onClose(false);
 
3
  import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
4
  import { CheckCheck } from "lucide-react";
5
  import { isTheSameHtml } from "@/lib/compare-html-diff";
6
+ import { Page } from "@/types";
7
 
8
  export const ProModal = ({
9
  open,
10
+ pages,
11
  onClose,
12
  }: {
13
  open: boolean;
14
+ pages: Page[];
15
  onClose: React.Dispatch<React.SetStateAction<boolean>>;
16
  }) => {
17
+ const [, setStorage] = useLocalStorage("pages");
18
  const handleProClick = () => {
19
+ if (pages && !isTheSameHtml(pages?.[0].html)) {
20
+ setStorage(pages);
21
  }
22
  window.open("https://huggingface.co/subscribe/pro?from=DeepSite", "_blank");
23
  onClose(false);
components/ui/button.tsx CHANGED
@@ -33,6 +33,7 @@ const buttonVariants = cva(
33
  icon: "size-9",
34
  iconXs: "size-7",
35
  iconXss: "size-6",
 
36
  xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1",
37
  },
38
  },
 
33
  icon: "size-9",
34
  iconXs: "size-7",
35
  iconXss: "size-6",
36
+ iconXsss: "size-5",
37
  xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1",
38
  },
39
  },
hooks/useCallAi.ts ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from "react";
2
+ import { toast } from "sonner";
3
+ import { useLocalStorage } from "react-use";
4
+ import { MODELS } from "@/lib/providers";
5
+ import { Page } from "@/types";
6
+
7
+ interface UseCallAiProps {
8
+ onNewPrompt: (prompt: string) => void;
9
+ onSuccess: (page: Page[], p: string, n?: number[][]) => void;
10
+ onScrollToBottom: () => void;
11
+ setPages: React.Dispatch<React.SetStateAction<Page[]>>;
12
+ setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
13
+ currentPage: Page;
14
+ pages: Page[];
15
+ isAiWorking: boolean;
16
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
17
+ }
18
+
19
+ export const useCallAi = ({
20
+ onNewPrompt,
21
+ onSuccess,
22
+ onScrollToBottom,
23
+ setPages,
24
+ setCurrentPage,
25
+ currentPage,
26
+ pages,
27
+ isAiWorking,
28
+ setisAiWorking,
29
+ }: UseCallAiProps) => {
30
+ const audio = useRef<HTMLAudioElement | null>(null);
31
+ const [provider] = useLocalStorage("provider", "auto");
32
+ const [model] = useLocalStorage("model", MODELS[0].value);
33
+ const [controller, setController] = useState<AbortController | null>(null);
34
+
35
+ const callAiNewProject = async (prompt: string, redesignMarkdown?: string, handleThink?: (think: string) => void, onFinishThink?: () => void) => {
36
+ if (isAiWorking) return;
37
+ if (!redesignMarkdown && !prompt.trim()) return;
38
+
39
+ setisAiWorking(true);
40
+
41
+ const abortController = new AbortController();
42
+ setController(abortController);
43
+
44
+ try {
45
+ onNewPrompt(prompt);
46
+
47
+ const request = await fetch("/api/ask-ai", {
48
+ method: "POST",
49
+ body: JSON.stringify({
50
+ prompt,
51
+ provider,
52
+ model,
53
+ redesignMarkdown,
54
+ }),
55
+ headers: {
56
+ "Content-Type": "application/json",
57
+ "x-forwarded-for": window.location.hostname,
58
+ },
59
+ signal: abortController.signal,
60
+ });
61
+
62
+ if (request && request.body) {
63
+ const reader = request.body.getReader();
64
+ const decoder = new TextDecoder("utf-8");
65
+ const selectedModel = MODELS.find(
66
+ (m: { value: string }) => m.value === model
67
+ );
68
+ let contentResponse = "";
69
+
70
+ const read = async () => {
71
+ const { done, value } = await reader.read();
72
+ if (done) {
73
+ const isJson =
74
+ contentResponse.trim().startsWith("{") &&
75
+ contentResponse.trim().endsWith("}");
76
+ const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
77
+
78
+ if (jsonResponse && !jsonResponse.ok) {
79
+ if (jsonResponse.openLogin) {
80
+ // Handle login required
81
+ return { error: "login_required" };
82
+ } else if (jsonResponse.openSelectProvider) {
83
+ // Handle provider selection required
84
+ return { error: "provider_required", message: jsonResponse.message };
85
+ } else if (jsonResponse.openProModal) {
86
+ // Handle pro modal required
87
+ return { error: "pro_required" };
88
+ } else {
89
+ toast.error(jsonResponse.message);
90
+ setisAiWorking(false);
91
+ return { error: "api_error", message: jsonResponse.message };
92
+ }
93
+ }
94
+
95
+ toast.success("AI responded successfully");
96
+ setisAiWorking(false);
97
+
98
+ if (audio.current) audio.current.play();
99
+
100
+ const newPages = formatPages(contentResponse);
101
+ onSuccess(newPages, prompt);
102
+
103
+ return { success: true, pages: newPages };
104
+ }
105
+
106
+ const chunk = decoder.decode(value, { stream: true });
107
+ contentResponse += chunk;
108
+
109
+ if (selectedModel?.isThinker) {
110
+ const thinkMatch = contentResponse.match(/<think>[\s\S]*/)?.[0];
111
+ if (thinkMatch && !contentResponse?.includes("</think>")) {
112
+ handleThink?.(thinkMatch.replace("<think>", "").trim());
113
+ return read();
114
+ }
115
+ }
116
+
117
+ if (contentResponse.includes("</think>")) {
118
+ onFinishThink?.();
119
+ }
120
+
121
+ formatPages(contentResponse);
122
+ return read();
123
+ };
124
+
125
+ return await read();
126
+ }
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ } catch (error: any) {
129
+ setisAiWorking(false);
130
+ toast.error(error.message);
131
+ if (error.openLogin) {
132
+ return { error: "login_required" };
133
+ }
134
+ return { error: "network_error", message: error.message };
135
+ }
136
+ };
137
+
138
+ const callAiNewPage = async (prompt: string, currentPagePath: string, previousPrompts?: string[]) => {
139
+ if (isAiWorking) return;
140
+ if (!prompt.trim()) return;
141
+
142
+ setisAiWorking(true);
143
+
144
+ const abortController = new AbortController();
145
+ setController(abortController);
146
+
147
+ try {
148
+ onNewPrompt(prompt);
149
+
150
+ const request = await fetch("/api/ask-ai", {
151
+ method: "POST",
152
+ body: JSON.stringify({
153
+ prompt,
154
+ provider,
155
+ model,
156
+ pages,
157
+ previousPrompts,
158
+ }),
159
+ headers: {
160
+ "Content-Type": "application/json",
161
+ "x-forwarded-for": window.location.hostname,
162
+ },
163
+ signal: abortController.signal,
164
+ });
165
+
166
+ if (request && request.body) {
167
+ const reader = request.body.getReader();
168
+ const decoder = new TextDecoder("utf-8");
169
+ const selectedModel = MODELS.find(
170
+ (m: { value: string }) => m.value === model
171
+ );
172
+ let contentResponse = "";
173
+
174
+ const read = async () => {
175
+ const { done, value } = await reader.read();
176
+ if (done) {
177
+ const isJson =
178
+ contentResponse.trim().startsWith("{") &&
179
+ contentResponse.trim().endsWith("}");
180
+ const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
181
+
182
+ if (jsonResponse && !jsonResponse.ok) {
183
+ if (jsonResponse.openLogin) {
184
+ // Handle login required
185
+ return { error: "login_required" };
186
+ } else if (jsonResponse.openSelectProvider) {
187
+ // Handle provider selection required
188
+ return { error: "provider_required", message: jsonResponse.message };
189
+ } else if (jsonResponse.openProModal) {
190
+ // Handle pro modal required
191
+ return { error: "pro_required" };
192
+ } else {
193
+ toast.error(jsonResponse.message);
194
+ setisAiWorking(false);
195
+ return { error: "api_error", message: jsonResponse.message };
196
+ }
197
+ }
198
+
199
+ toast.success("AI responded successfully");
200
+ setisAiWorking(false);
201
+
202
+ if (selectedModel?.isThinker) {
203
+ // Reset to default model if using thinker model
204
+ // Note: You might want to add a callback for this
205
+ }
206
+
207
+ if (audio.current) audio.current.play();
208
+
209
+ const newPage = formatPage(contentResponse, currentPagePath);
210
+ if (!newPage) { return { error: "api_error", message: "Failed to format page" } }
211
+ onSuccess([...pages, newPage], prompt);
212
+
213
+ return { success: true, pages: [...pages, newPage] };
214
+ }
215
+
216
+ const chunk = decoder.decode(value, { stream: true });
217
+ contentResponse += chunk;
218
+
219
+ if (selectedModel?.isThinker) {
220
+ const thinkMatch = contentResponse.match(/<think>[\s\S]*/)?.[0];
221
+ if (thinkMatch && !contentResponse?.includes("</think>")) {
222
+ // contentThink += chunk;
223
+ return read();
224
+ }
225
+ }
226
+
227
+ formatPage(contentResponse, currentPagePath);
228
+ return read();
229
+ };
230
+
231
+ return await read();
232
+ }
233
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
234
+ } catch (error: any) {
235
+ setisAiWorking(false);
236
+ toast.error(error.message);
237
+ if (error.openLogin) {
238
+ return { error: "login_required" };
239
+ }
240
+ return { error: "network_error", message: error.message };
241
+ }
242
+ };
243
+
244
+ const callAiFollowUp = async (prompt: string, previousPrompt: string, selectedElementHtml?: string) => {
245
+ if (isAiWorking) return;
246
+ if (!prompt.trim()) return;
247
+
248
+ setisAiWorking(true);
249
+
250
+ const abortController = new AbortController();
251
+ setController(abortController);
252
+
253
+ try {
254
+ onNewPrompt(prompt);
255
+
256
+ const request = await fetch("/api/ask-ai", {
257
+ method: "PUT",
258
+ body: JSON.stringify({
259
+ prompt,
260
+ provider,
261
+ previousPrompt,
262
+ model,
263
+ html: currentPage?.html,
264
+ pages,
265
+ selectedElementHtml,
266
+ }),
267
+ headers: {
268
+ "Content-Type": "application/json",
269
+ "x-forwarded-for": window.location.hostname,
270
+ },
271
+ signal: abortController.signal,
272
+ });
273
+
274
+ if (request && request.body) {
275
+ const res = await request.json();
276
+
277
+ if (!request.ok) {
278
+ if (res.openLogin) {
279
+ setisAiWorking(false);
280
+ return { error: "login_required" };
281
+ } else if (res.openSelectProvider) {
282
+ setisAiWorking(false);
283
+ return { error: "provider_required", message: res.message };
284
+ } else if (res.openProModal) {
285
+ setisAiWorking(false);
286
+ return { error: "pro_required" };
287
+ } else {
288
+ toast.error(res.message);
289
+ setisAiWorking(false);
290
+ return { error: "api_error", message: res.message };
291
+ }
292
+ }
293
+
294
+ toast.success("AI responded successfully");
295
+ setisAiWorking(false);
296
+
297
+ setPages(res.pages);
298
+ onSuccess(res.pages, prompt, res.updatedLines);
299
+
300
+ if (audio.current) audio.current.play();
301
+
302
+ return { success: true, html: res.html, updatedLines: res.updatedLines };
303
+ }
304
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
305
+ } catch (error: any) {
306
+ setisAiWorking(false);
307
+ toast.error(error.message);
308
+ if (error.openLogin) {
309
+ return { error: "login_required" };
310
+ }
311
+ return { error: "network_error", message: error.message };
312
+ }
313
+ };
314
+
315
+ // Stop the current AI generation
316
+ const stopController = () => {
317
+ if (controller) {
318
+ controller.abort();
319
+ setController(null);
320
+ setisAiWorking(false);
321
+ }
322
+ };
323
+
324
+ const formatPages = (content: string) => {
325
+ const pages: Page[] = [];
326
+ if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) {
327
+ return pages;
328
+ }
329
+
330
+ const cleanedContent = content.replace(
331
+ /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/,
332
+ "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE"
333
+ );
334
+ const htmlChunks = cleanedContent.split(
335
+ /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/
336
+ );
337
+ const processedChunks = new Set<number>();
338
+
339
+ htmlChunks.forEach((chunk, index) => {
340
+ if (processedChunks.has(index) || !chunk?.trim()) {
341
+ return;
342
+ }
343
+ const htmlContent = extractHtmlContent(htmlChunks[index + 1]);
344
+
345
+ if (htmlContent) {
346
+ const page: Page = {
347
+ path: chunk.trim(),
348
+ html: htmlContent,
349
+ };
350
+ pages.push(page);
351
+
352
+ if (htmlContent.length > 200) {
353
+ onScrollToBottom();
354
+ }
355
+
356
+ processedChunks.add(index);
357
+ processedChunks.add(index + 1);
358
+ }
359
+ });
360
+ if (pages.length > 0) {
361
+ setPages(pages);
362
+ const lastPagePath = pages[pages.length - 1]?.path;
363
+ setCurrentPage(lastPagePath || "index.html");
364
+ }
365
+
366
+ return pages;
367
+ };
368
+
369
+ const formatPage = (content: string, currentPagePath: string) => {
370
+ if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) {
371
+ return null;
372
+ }
373
+
374
+ const cleanedContent = content.replace(
375
+ /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/,
376
+ "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE"
377
+ );
378
+
379
+ const htmlChunks = cleanedContent.split(
380
+ /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/
381
+ )?.filter(Boolean);
382
+
383
+ const pagePath = htmlChunks[0]?.trim() || "";
384
+ const htmlContent = extractHtmlContent(htmlChunks[1]);
385
+
386
+ if (!pagePath || !htmlContent) {
387
+ return null;
388
+ }
389
+
390
+ const page: Page = {
391
+ path: pagePath,
392
+ html: htmlContent,
393
+ };
394
+
395
+ setPages(prevPages => {
396
+ const existingPageIndex = prevPages.findIndex(p => p.path === currentPagePath || p.path === pagePath);
397
+
398
+ if (existingPageIndex !== -1) {
399
+ const updatedPages = [...prevPages];
400
+ updatedPages[existingPageIndex] = page;
401
+ return updatedPages;
402
+ } else {
403
+ return [...prevPages, page];
404
+ }
405
+ });
406
+
407
+ setCurrentPage(pagePath);
408
+
409
+ if (htmlContent.length > 200) {
410
+ onScrollToBottom();
411
+ }
412
+
413
+ return page;
414
+ };
415
+
416
+ // Helper function to extract and clean HTML content
417
+ const extractHtmlContent = (chunk: string): string => {
418
+ if (!chunk) return "";
419
+
420
+ // Extract HTML content
421
+ const htmlMatch = chunk.trim().match(/<!DOCTYPE html>[\s\S]*/);
422
+ if (!htmlMatch) return "";
423
+
424
+ let htmlContent = htmlMatch[0];
425
+
426
+ // Ensure proper HTML structure
427
+ htmlContent = ensureCompleteHtml(htmlContent);
428
+
429
+ // Remove markdown code blocks if present
430
+ htmlContent = htmlContent.replace(/```/g, "");
431
+
432
+ return htmlContent;
433
+ };
434
+
435
+ // Helper function to ensure HTML has complete structure
436
+ const ensureCompleteHtml = (html: string): string => {
437
+ let completeHtml = html;
438
+
439
+ // Add missing head closing tag
440
+ if (completeHtml.includes("<head>") && !completeHtml.includes("</head>")) {
441
+ completeHtml += "\n</head>";
442
+ }
443
+
444
+ // Add missing body closing tag
445
+ if (completeHtml.includes("<body") && !completeHtml.includes("</body>")) {
446
+ completeHtml += "\n</body>";
447
+ }
448
+
449
+ // Add missing html closing tag
450
+ if (!completeHtml.includes("</html>")) {
451
+ completeHtml += "\n</html>";
452
+ }
453
+
454
+ return completeHtml;
455
+ };
456
+
457
+ return {
458
+ callAiNewProject,
459
+ callAiFollowUp,
460
+ callAiNewPage,
461
+ stopController,
462
+ controller,
463
+ audio,
464
+ };
465
+ };
hooks/useEditor.ts CHANGED
@@ -1,12 +1,18 @@
1
- import { HtmlHistory } from "@/types";
 
2
  import { useState } from "react";
3
 
4
- export const useEditor = (defaultHtml: string) => {
5
  /**
6
  * State to manage the HTML content of the editor.
7
  * This will be the main content that users edit.
8
  */
9
- const [html, setHtml] = useState(defaultHtml);
 
 
 
 
 
10
  /**
11
  * State to manage the history of HTML edits.
12
  * This will store previous versions of the HTML content along with metadata. (not saved to DB)
@@ -19,12 +25,13 @@ export const useEditor = (defaultHtml: string) => {
19
  */
20
  const [prompts, setPrompts] = useState<string[]>([]);
21
 
 
22
  return {
23
- html,
24
- setHtml,
25
  htmlHistory,
26
  setHtmlHistory,
27
  prompts,
 
 
28
  setPrompts,
29
  };
30
  };
 
1
+ import { defaultHTML } from "@/lib/consts";
2
+ import { HtmlHistory, Page } from "@/types";
3
  import { useState } from "react";
4
 
5
+ export const useEditor = (initialPages?: Page[]) => {
6
  /**
7
  * State to manage the HTML content of the editor.
8
  * This will be the main content that users edit.
9
  */
10
+ const [pages, setPages] = useState<Array<Page>>(initialPages ??[
11
+ {
12
+ path: "index.html",
13
+ html: defaultHTML,
14
+ },
15
+ ]);
16
  /**
17
  * State to manage the history of HTML edits.
18
  * This will store previous versions of the HTML content along with metadata. (not saved to DB)
 
25
  */
26
  const [prompts, setPrompts] = useState<string[]>([]);
27
 
28
+
29
  return {
 
 
30
  htmlHistory,
31
  setHtmlHistory,
32
  prompts,
33
+ pages,
34
+ setPages,
35
  setPrompts,
36
  };
37
  };
lib/prompts.ts CHANGED
@@ -2,24 +2,73 @@ export const SEARCH_START = "<<<<<<< SEARCH";
2
  export const DIVIDER = "=======";
3
  export const REPLACE_END = ">>>>>>> REPLACE";
4
  export const MAX_REQUESTS_PER_IP = 2;
5
- export const INITIAL_SYSTEM_PROMPT = `ONLY USE HTML, CSS AND JAVASCRIPT. If you want to use ICON make sure to import the library first. Try to create the best UI possible by using only HTML, CSS and JAVASCRIPT. MAKE IT RESPONSIVE USING TAILWINDCSS. Use as much as you can TailwindCSS for the CSS, if you can't do something with TailwindCSS, then use custom CSS (make sure to import <script src="https://cdn.tailwindcss.com"></script> in the head). Also, try to ellaborate as much as you can, to create something unique. ALWAYS GIVE THE RESPONSE INTO A SINGLE HTML FILE. AVOID CHINESE CHARACTERS IN THE CODE IF NOT ASKED BY THE USER.`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert web developer modifying an existing HTML file.
7
  The user wants to apply changes based on their request.
8
  You MUST output ONLY the changes required using the following SEARCH/REPLACE block format. Do NOT output the entire file.
9
  Explain the changes briefly *before* the blocks if necessary, but the code changes THEMSELVES MUST be within the blocks.
10
  Format Rules:
11
- 1. Start with ${SEARCH_START}
12
- 2. Provide the exact lines from the current code that need to be replaced.
13
- 3. Use ${DIVIDER} to separate the search block from the replacement.
14
- 4. Provide the new lines that should replace the original lines.
15
- 5. End with ${REPLACE_END}
16
- 6. You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the file.
17
- 7. To insert code, use an empty SEARCH block (only ${SEARCH_START} and ${DIVIDER} on their lines) if inserting at the very beginning, otherwise provide the line *before* the insertion point in the SEARCH block and include that line plus the new lines in the REPLACE block.
18
- 8. To delete code, provide the lines to delete in the SEARCH block and leave the REPLACE block empty (only ${DIVIDER} and ${REPLACE_END} on their lines).
19
- 9. IMPORTANT: The SEARCH block must *exactly* match the current code, including indentation and whitespace.
 
 
 
20
  Example Modifying Code:
21
  \`\`\`
22
  Some explanation...
 
23
  ${SEARCH_START}
24
  <h1>Old Title</h1>
25
  ${DIVIDER}
@@ -35,8 +84,39 @@ ${REPLACE_END}
35
  Example Deleting Code:
36
  \`\`\`
37
  Removing the paragraph...
 
38
  ${SEARCH_START}
39
  <p>This paragraph will be deleted.</p>
40
  ${DIVIDER}
41
  ${REPLACE_END}
42
- \`\`\``;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  export const DIVIDER = "=======";
3
  export const REPLACE_END = ">>>>>>> REPLACE";
4
  export const MAX_REQUESTS_PER_IP = 2;
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
+
12
+ export const INITIAL_SYSTEM_PROMPT = `Only use HTML, CSS and Javascript.
13
+ If you want to use ICON make sure to import library first.
14
+ Try to create the best UI possible by using only HTML, CSS and Javascript.
15
+ Make it responsive using TailwindCSS. Use as much as you can TailwindCSS for the CSS, if you can't do something with TailwindCSS, then use custom CSS (make sure to import <script src="https://cdn.tailwindcss.com"></script> in the head).
16
+ Also, try to elaborate as much as you can, to create something unique.
17
+ If you want to use on scroll animation, import AOS. (make sure to add <link href="https://unpkg.com/[email protected]/dist/aos.css" rel="stylesheet"> and <script src="https://unpkg.com/[email protected]/dist/aos.js"></script> and <script>AOS.init();</script>)
18
+ Returns the result in a \`\`\`html\`\`\` markdown. If the user doesn't ask for a different pages, do it as a Single page. Format the results like:
19
+ 1. Start with ${TITLE_PAGE_START}.
20
+ 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.
21
+ 3. Close the start tag with the ${TITLE_PAGE_END}.
22
+ 4. Start the HTML response with the triple backticks, like \`\`\`html.
23
+ 5. Insert the following html there.
24
+ 6. Close with the triple backticks, like \`\`\`.
25
+ 7. Retry if another pages.
26
+ Example Code:
27
+ ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
28
+ \`\`\`html
29
+ <!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>Index</title>
35
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
36
+ <script src="https://cdn.tailwindcss.com"></script>
37
+ <link href="https://unpkg.com/[email protected]/dist/aos.css" rel="stylesheet">
38
+ <script src="https://unpkg.com/[email protected]/dist/aos.js"></script>
39
+ </head>
40
+ <body>
41
+ <h1>Hello World</h1>
42
+ <script>AOS.init();</script>
43
+ </body>
44
+ </html>
45
+ \`\`\`
46
+ The first file should be always named index.html. Also, if there are more than 1 page, dont forget to includes the page in a link <a href="page.html">page</a> to be accessible (Dont use onclick to navigate, only href). Can be in a menu, button or whatever you want.
47
+ Avoid Chinese characters in the code if not asked by the user.
48
+ xf`;
49
+
50
+
51
  export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert web developer modifying an existing HTML file.
52
  The user wants to apply changes based on their request.
53
  You MUST output ONLY the changes required using the following SEARCH/REPLACE block format. Do NOT output the entire file.
54
  Explain the changes briefly *before* the blocks if necessary, but the code changes THEMSELVES MUST be within the blocks.
55
  Format Rules:
56
+ 1. Start with ${UPDATE_PAGE_START}
57
+ 2. Provide the name of the page you are modifying.
58
+ 3. Close the start tag with the ${UPDATE_PAGE_END}.
59
+ 4. Start with ${SEARCH_START}
60
+ 5. Provide the exact lines from the current code that need to be replaced.
61
+ 6. Use ${DIVIDER} to separate the search block from the replacement.
62
+ 7. Provide the new lines that should replace the original lines.
63
+ 8. End with ${REPLACE_END}
64
+ 9. You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the file.
65
+ 10. To insert code, use an empty SEARCH block (only ${SEARCH_START} and ${DIVIDER} on their lines) if inserting at the very beginning, otherwise provide the line *before* the insertion point in the SEARCH block and include that line plus the new lines in the REPLACE block.
66
+ 11. To delete code, provide the lines to delete in the SEARCH block and leave the REPLACE block empty (only ${DIVIDER} and ${REPLACE_END} on their lines).
67
+ 12. IMPORTANT: The SEARCH block must *exactly* match the current code, including indentation and whitespace.
68
  Example Modifying Code:
69
  \`\`\`
70
  Some explanation...
71
+ ${UPDATE_PAGE_START}index.html${UPDATE_PAGE_END}
72
  ${SEARCH_START}
73
  <h1>Old Title</h1>
74
  ${DIVIDER}
 
84
  Example Deleting Code:
85
  \`\`\`
86
  Removing the paragraph...
87
+ ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
88
  ${SEARCH_START}
89
  <p>This paragraph will be deleted.</p>
90
  ${DIVIDER}
91
  ${REPLACE_END}
92
+ \`\`\`
93
+ The user can also ask to add a new page, in this case you should return the new page in the following format:
94
+ 1. Start with ${NEW_PAGE_START}.
95
+ 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.
96
+ 3. Close the start tag with the ${NEW_PAGE_END}.
97
+ 4. Start the HTML response with the triple backticks, like \`\`\`html.
98
+ 5. Insert the following html there.
99
+ 6. Close with the triple backticks, like \`\`\`.
100
+ 7. Retry if another pages.
101
+ Example Code:
102
+ ${NEW_PAGE_START}index.html${NEW_PAGE_END}
103
+ \`\`\`html
104
+ <!DOCTYPE html>
105
+ <html lang="en">
106
+ <head>
107
+ <meta charset="UTF-8">
108
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
109
+ <title>Index</title>
110
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
111
+ <script src="https://cdn.tailwindcss.com"></script>
112
+ <link href="https://unpkg.com/[email protected]/dist/aos.css" rel="stylesheet">
113
+ <script src="https://unpkg.com/[email protected]/dist/aos.js"></script>
114
+ </head>
115
+ <body>
116
+ <h1>Hello World</h1>
117
+ <script>AOS.init();</script>
118
+ </body>
119
+ </html>
120
+ \`\`\`
121
+ Also, if there are more than 1 page, dont forget to includes the page in a link <a href="page.html">page</a> to be accessible (Dont use onclick to navigate, only href).
122
+ No need to explain what you did. Just return the expected result.`;
lib/providers.ts CHANGED
@@ -70,4 +70,12 @@ export const MODELS = [
70
  providers: ["together", "novita", "groq"],
71
  autoProvider: "groq",
72
  },
 
 
 
 
 
 
 
 
73
  ];
 
70
  providers: ["together", "novita", "groq"],
71
  autoProvider: "groq",
72
  },
73
+ {
74
+ value: "deepseek-ai/DeepSeek-V3.1",
75
+ label: "DeepSeek V3.1",
76
+ providers: ["fireworks-ai", "novita"],
77
+ isNew: true,
78
+ isThinker: true,
79
+ autoProvider: "fireworks-ai",
80
+ },
81
  ];
types/index.ts CHANGED
@@ -9,7 +9,7 @@ export interface User {
9
  }
10
 
11
  export interface HtmlHistory {
12
- html: string;
13
  createdAt: Date;
14
  prompt: string;
15
  }
@@ -24,3 +24,8 @@ export interface Project {
24
  _updatedAt?: Date;
25
  _createdAt?: Date;
26
  }
 
 
 
 
 
 
9
  }
10
 
11
  export interface HtmlHistory {
12
+ pages: Page[];
13
  createdAt: Date;
14
  prompt: string;
15
  }
 
24
  _updatedAt?: Date;
25
  _createdAt?: Date;
26
  }
27
+
28
+ export interface Page {
29
+ path: string;
30
+ html: string;
31
+ }