app/api/ask-ai/route.ts CHANGED
@@ -10,15 +10,10 @@ import {
10
  FOLLOW_UP_SYSTEM_PROMPT,
11
  INITIAL_SYSTEM_PROMPT,
12
  MAX_REQUESTS_PER_IP,
13
- NEW_PAGE_END,
14
- NEW_PAGE_START,
15
  REPLACE_END,
16
  SEARCH_START,
17
- UPDATE_PAGE_START,
18
- UPDATE_PAGE_END,
19
  } 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,7 +22,7 @@ export async function POST(request: NextRequest) {
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,7 +34,6 @@ export async function POST(request: NextRequest) {
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,10 +92,12 @@ export async function POST(request: NextRequest) {
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,7 +107,7 @@ export async function POST(request: NextRequest) {
111
  });
112
 
113
  (async () => {
114
- // let completeResponse = "";
115
  try {
116
  const client = new InferenceClient(token);
117
  const chatCompletion = client.chatCompletionStream(
@@ -123,14 +119,12 @@ export async function POST(request: NextRequest) {
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,7 +141,39 @@ export async function POST(request: NextRequest) {
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,10 +223,10 @@ export async function PUT(request: NextRequest) {
197
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
198
 
199
  const body = await request.json();
200
- const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, } =
201
  body;
202
 
203
- if (!prompt || pages.length === 0) {
204
  return NextResponse.json(
205
  { ok: false, error: "Missing required fields" },
206
  { status: 400 }
@@ -270,18 +296,18 @@ export async function PUT(request: NextRequest) {
270
  },
271
  {
272
  role: "user",
273
- content: previousPrompts
274
- ? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
275
  : "You are modifying the HTML file based on the user's request.",
276
  },
277
  {
278
  role: "assistant",
279
 
280
- content: `${
281
  selectedElementHtml
282
  ? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
283
  : ""
284
- }. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}. ${files?.length > 0 ? `Current images: ${files?.map((f: string) => `- ${f}`).join("\n")}.` : ""}`,
285
  },
286
  {
287
  role: "user",
@@ -307,171 +333,61 @@ export async function PUT(request: NextRequest) {
307
 
308
  if (chunk) {
309
  const updatedLines: number[][] = [];
310
- let newHtml = "";
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(
 
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
  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
  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
  : 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
  });
108
 
109
  (async () => {
110
+ let completeResponse = "";
111
  try {
112
  const client = new InferenceClient(token);
113
  const chatCompletion = client.chatCompletionStream(
 
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
 
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
  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) {
230
  return NextResponse.json(
231
  { ok: false, error: "Missing required fields" },
232
  { status: 400 }
 
296
  },
297
  {
298
  role: "user",
299
+ content: previousPrompt
300
+ ? previousPrompt
301
  : "You are modifying the HTML file based on the user's request.",
302
  },
303
  {
304
  role: "assistant",
305
 
306
+ content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
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",
 
333
 
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(
app/api/me/projects/[namespace]/[repoId]/images/route.ts DELETED
@@ -1,111 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { RepoDesignation, uploadFiles } from "@huggingface/hub";
3
-
4
- import { isAuthenticated } from "@/lib/auth";
5
- import Project from "@/models/Project";
6
- import dbConnect from "@/lib/mongodb";
7
-
8
- // No longer need the ImageUpload interface since we're handling FormData with File objects
9
-
10
- export async function POST(
11
- req: NextRequest,
12
- { params }: { params: Promise<{ namespace: string; repoId: string }> }
13
- ) {
14
- try {
15
- const user = await isAuthenticated();
16
-
17
- if (user instanceof NextResponse || !user) {
18
- return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
19
- }
20
-
21
- await dbConnect();
22
- const param = await params;
23
- const { namespace, repoId } = param;
24
-
25
- const project = await Project.findOne({
26
- user_id: user.id,
27
- space_id: `${namespace}/${repoId}`,
28
- }).lean();
29
-
30
- if (!project) {
31
- return NextResponse.json(
32
- {
33
- ok: false,
34
- error: "Project not found",
35
- },
36
- { status: 404 }
37
- );
38
- }
39
-
40
- // Parse the FormData to get the images
41
- const formData = await req.formData();
42
- const imageFiles = formData.getAll("images") as File[];
43
-
44
- if (!imageFiles || imageFiles.length === 0) {
45
- return NextResponse.json(
46
- {
47
- ok: false,
48
- error: "At least one image file is required under the 'images' key",
49
- },
50
- { status: 400 }
51
- );
52
- }
53
-
54
- const files: File[] = [];
55
- for (const file of imageFiles) {
56
- if (!(file instanceof File)) {
57
- return NextResponse.json(
58
- {
59
- ok: false,
60
- error: "Invalid file format - all items under 'images' key must be files",
61
- },
62
- { status: 400 }
63
- );
64
- }
65
-
66
- if (!file.type.startsWith('image/')) {
67
- return NextResponse.json(
68
- {
69
- ok: false,
70
- error: `File ${file.name} is not an image`,
71
- },
72
- { status: 400 }
73
- );
74
- }
75
-
76
- // Create File object with images/ folder prefix
77
- const fileName = `images/${file.name}`;
78
- const processedFile = new File([file], fileName, { type: file.type });
79
- files.push(processedFile);
80
- }
81
-
82
- // Upload files to HuggingFace space
83
- const repo: RepoDesignation = {
84
- type: "space",
85
- name: `${namespace}/${repoId}`,
86
- };
87
-
88
- await uploadFiles({
89
- repo,
90
- files,
91
- accessToken: user.token as string,
92
- commitTitle: `Upload ${files.length} image(s)`,
93
- });
94
-
95
- return NextResponse.json({
96
- ok: true,
97
- message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
98
- uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
99
- }, { status: 200 });
100
-
101
- } catch (error) {
102
- console.error('Error uploading images:', error);
103
- return NextResponse.json(
104
- {
105
- ok: false,
106
- error: "Failed to upload images",
107
- },
108
- { status: 500 }
109
- );
110
- }
111
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/me/projects/[namespace]/[repoId]/route.ts CHANGED
@@ -1,10 +1,10 @@
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,6 +33,7 @@ export async function GET(
33
  { status: 404 }
34
  );
35
  }
 
36
  try {
37
  const space = await spaceInfo({
38
  name: namespace + "/" + repoId,
@@ -59,59 +60,25 @@ export async function GET(
59
  );
60
  }
61
 
62
- const repo: RepoDesignation = {
63
- type: "space",
64
- name: `${namespace}/${repoId}`,
65
- };
66
-
67
- const htmlFiles: Page[] = [];
68
- const images: string[] = [];
69
-
70
- const allowedImagesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
71
-
72
- for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
73
- if (fileInfo.path.endsWith(".html")) {
74
- const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
75
- if (res.ok) {
76
- const html = await res.text();
77
- if (fileInfo.path === "index.html") {
78
- htmlFiles.unshift({
79
- path: fileInfo.path,
80
- html,
81
- });
82
- } else {
83
- htmlFiles.push({
84
- path: fileInfo.path,
85
- html,
86
- });
87
- }
88
- }
89
- }
90
- if (fileInfo.type === "directory" && fileInfo.path === "images") {
91
- for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
92
- if (allowedImagesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
93
- images.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
94
- }
95
- }
96
- }
97
- }
98
-
99
- if (htmlFiles.length === 0) {
100
  return NextResponse.json(
101
  {
102
  ok: false,
103
- error: "No HTML files found",
104
  },
105
  { status: 404 }
106
  );
107
  }
 
 
 
108
 
109
  return NextResponse.json(
110
  {
111
  project: {
112
  ...project,
113
- pages: htmlFiles,
114
- images,
115
  },
116
  ok: true,
117
  },
@@ -150,7 +117,7 @@ export async function PUT(
150
  await dbConnect();
151
  const param = await params;
152
  const { namespace, repoId } = param;
153
- const { pages, prompts } = await req.json();
154
 
155
  const project = await Project.findOne({
156
  user_id: user.id,
@@ -171,18 +138,11 @@ export async function PUT(
171
  name: `${namespace}/${repoId}`,
172
  };
173
 
174
- const files: File[] = [];
175
- const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
176
- type: "text/plain",
177
- });
178
- files.push(promptsFile);
179
- pages.forEach((page: Page) => {
180
- const file = new File([page.html], page.path, { type: "text/html" });
181
- files.push(file);
182
- });
183
- await uploadFiles({
184
  repo,
185
- files,
186
  accessToken: user.token as string,
187
  commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
188
  });
@@ -192,6 +152,7 @@ export async function PUT(
192
  {
193
  $set: {
194
  prompts: [
 
195
  ...prompts,
196
  ],
197
  },
 
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
  { 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
  );
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
  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
  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
  });
 
152
  {
153
  $set: {
154
  prompts: [
155
+ ...(project && "prompts" in project ? project.prompts : []),
156
  ...prompts,
157
  ],
158
  },
app/api/me/projects/route.ts CHANGED
@@ -4,9 +4,8 @@ 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 } from "@/lib/utils";
8
- import { Page } from "@/types";
9
-
10
  export async function GET() {
11
  const user = await isAuthenticated();
12
 
@@ -40,6 +39,10 @@ export async function GET() {
40
  );
41
  }
42
 
 
 
 
 
43
  export async function POST(request: NextRequest) {
44
  const user = await isAuthenticated();
45
 
@@ -47,9 +50,9 @@ export async function POST(request: NextRequest) {
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,6 +63,7 @@ export async function POST(request: NextRequest) {
60
 
61
  try {
62
  let readme = "";
 
63
 
64
  const newTitle = title
65
  .toLowerCase()
@@ -93,17 +97,12 @@ tags:
93
 
94
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
95
 
 
 
96
  const readmeFile = new File([readme], "README.md", {
97
  type: "text/markdown",
98
  });
99
- const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
100
- type: "text/plain",
101
- });
102
- const files = [readmeFile, promptsFile];
103
- pages.forEach((page: Page) => {
104
- const file = new File([page.html], page.path, { type: "text/html" });
105
- files.push(file);
106
- });
107
  await uploadFiles({
108
  repo,
109
  files,
 
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
  );
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
  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
 
64
  try {
65
  let readme = "";
66
+ let newHtml = html;
67
 
68
  const newTitle = title
69
  .toLowerCase()
 
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,
app/layout.tsx CHANGED
@@ -10,7 +10,6 @@ import MY_TOKEN_KEY from "@/lib/get-cookie-name";
10
  import { apiServer } from "@/lib/api";
11
  import AppContext from "@/components/contexts/app-context";
12
  import Script from "next/script";
13
- import IframeDetector from "@/components/iframe-detector";
14
 
15
  const inter = Inter({
16
  variable: "--font-inter-sans",
@@ -83,8 +82,6 @@ async function getMe() {
83
  }
84
  }
85
 
86
- // if domain isn't deepsite.hf.co or enzostvs-deepsite.hf.space redirect to deepsite.hf.co
87
-
88
  export default async function RootLayout({
89
  children,
90
  }: Readonly<{
@@ -101,7 +98,6 @@ export default async function RootLayout({
101
  <body
102
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
103
  >
104
- <IframeDetector />
105
  <Toaster richColors position="bottom-center" />
106
  <TanstackProvider>
107
  <AppContext me={data}>{children}</AppContext>
 
10
  import { apiServer } from "@/lib/api";
11
  import AppContext from "@/components/contexts/app-context";
12
  import Script from "next/script";
 
13
 
14
  const inter = Inter({
15
  variable: "--font-inter-sans",
 
82
  }
83
  }
84
 
 
 
85
  export default async function RootLayout({
86
  children,
87
  }: Readonly<{
 
98
  <body
99
  className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
100
  >
 
101
  <Toaster richColors position="bottom-center" />
102
  <TanstackProvider>
103
  <AppContext me={data}>{children}</AppContext>
app/projects/[namespace]/[repoId]/page.tsx CHANGED
@@ -32,11 +32,9 @@ export default async function ProjectNamespacePage({
32
  params: Promise<{ namespace: string; repoId: string }>;
33
  }) {
34
  const { namespace, repoId } = await params;
35
- const data = await getProject(namespace, repoId);
36
- if (!data?.pages) {
37
  redirect("/projects");
38
  }
39
- return (
40
- <AppEditor project={data} pages={data.pages} images={data.images ?? []} />
41
- );
42
  }
 
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
  }
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 isNew />;
5
  }
 
1
  import { AppEditor } from "@/components/editor";
2
 
3
  export default function ProjectsNewPage() {
4
+ return <AppEditor />;
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, useMemo, useRef } from "react";
4
  import classNames from "classnames";
5
  import { toast } from "sonner";
6
  import { useLocalStorage, useUpdateEffect } from "react-use";
@@ -10,8 +10,8 @@ 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, Page, Project } from "@/types";
14
- // import { InviteFriends } from "@/components/invite-friends";
15
  import { Settings } from "@/components/editor/ask-ai/settings";
16
  import { LoginModal } from "@/components/login-modal";
17
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
@@ -22,86 +22,50 @@ import { TooltipContent } from "@radix-ui/react-tooltip";
22
  import { SelectedHtmlElement } from "./selected-html-element";
23
  import { FollowUpTooltip } from "./follow-up-tooltip";
24
  import { isTheSameHtml } from "@/lib/compare-html-diff";
25
- import { useCallAi } from "@/hooks/useCallAi";
26
- import { SelectedFiles } from "./selected-files";
27
- import { Uploader } from "./uploader";
28
 
29
  export function AskAI({
30
- isNew,
31
- project,
32
- images,
33
- currentPage,
34
- previousPrompts,
35
  onScrollToBottom,
36
  isAiWorking,
37
  setisAiWorking,
38
  isEditableModeEnabled = false,
39
- pages,
40
- htmlHistory,
41
  selectedElement,
42
  setSelectedElement,
43
- selectedFiles,
44
- setSelectedFiles,
45
  setIsEditableModeEnabled,
46
  onNewPrompt,
47
  onSuccess,
48
- setPages,
49
- setCurrentPage,
50
  }: {
51
- project?: Project | null;
52
- currentPage: Page;
53
- images?: string[];
54
- pages: Page[];
55
  onScrollToBottom: () => void;
56
- previousPrompts: string[];
57
  isAiWorking: boolean;
58
  onNewPrompt: (prompt: string) => void;
59
  htmlHistory?: HtmlHistory[];
60
  setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
61
- isNew?: boolean;
62
- onSuccess: (page: Page[], p: string, n?: number[][]) => void;
63
  isEditableModeEnabled: boolean;
64
  setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
65
  selectedElement?: HTMLElement | null;
66
  setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
67
- selectedFiles: string[];
68
- setSelectedFiles: React.Dispatch<React.SetStateAction<string[]>>;
69
- setPages: React.Dispatch<React.SetStateAction<Page[]>>;
70
- setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
71
  }) {
72
  const refThink = useRef<HTMLDivElement | null>(null);
 
73
 
74
  const [open, setOpen] = useState(false);
75
  const [prompt, setPrompt] = useState("");
 
 
76
  const [provider, setProvider] = useLocalStorage("provider", "auto");
77
  const [model, setModel] = useLocalStorage("model", MODELS[0].value);
78
  const [openProvider, setOpenProvider] = useState(false);
79
  const [providerError, setProviderError] = useState("");
80
  const [openProModal, setOpenProModal] = useState(false);
 
81
  const [openThink, setOpenThink] = useState(false);
82
  const [isThinking, setIsThinking] = useState(true);
83
- const [think, setThink] = useState("");
84
  const [isFollowUp, setIsFollowUp] = useState(true);
85
- const [isUploading, setIsUploading] = useState(false);
86
- const [files, setFiles] = useState<string[]>(images ?? []);
87
-
88
- const {
89
- callAiNewProject,
90
- callAiFollowUp,
91
- callAiNewPage,
92
- stopController,
93
- audio: hookAudio,
94
- } = useCallAi({
95
- onNewPrompt,
96
- onSuccess,
97
- onScrollToBottom,
98
- setPages,
99
- setCurrentPage,
100
- currentPage,
101
- pages,
102
- isAiWorking,
103
- setisAiWorking,
104
- });
105
 
106
  const selectedModel = useMemo(() => {
107
  return MODELS.find((m: { value: string }) => m.value === model);
@@ -110,101 +74,203 @@ export function AskAI({
110
  const callAi = async (redesignMarkdown?: string) => {
111
  if (isAiWorking) return;
112
  if (!redesignMarkdown && !prompt.trim()) return;
 
 
 
 
 
113
 
114
- if (isFollowUp && !redesignMarkdown && !isSameHtml) {
115
- // Use follow-up function for existing projects
116
- const selectedElementHtml = selectedElement
117
- ? selectedElement.outerHTML
118
- : "";
119
 
120
- const result = await callAiFollowUp(
121
- prompt,
122
- model,
123
- provider,
124
- previousPrompts,
125
- selectedElementHtml,
126
- selectedFiles
127
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- if (result?.error) {
130
- handleError(result.error, result.message);
131
- return;
132
- }
 
 
 
 
 
133
 
134
- if (result?.success) {
135
- setPrompt("");
136
- }
137
- } else if (isFollowUp && pages.length > 1 && isSameHtml) {
138
- const result = await callAiNewPage(
139
- prompt,
140
- model,
141
- provider,
142
- currentPage.path,
143
- [
144
- ...(previousPrompts ?? []),
145
- ...(htmlHistory?.map((h) => h.prompt) ?? []),
146
- ]
147
- );
148
- if (result?.error) {
149
- handleError(result.error, result.message);
150
- return;
151
- }
152
 
153
- if (result?.success) {
154
- setPrompt("");
155
- }
156
- } else {
157
- const result = await callAiNewProject(
158
- prompt,
159
- model,
160
- provider,
161
- redesignMarkdown,
162
- handleThink,
163
- () => {
164
- setIsThinking(false);
165
- }
166
- );
167
 
168
- if (result?.error) {
169
- handleError(result.error, result.message);
170
- return;
171
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- if (result?.success) {
174
- setPrompt("");
175
- if (selectedModel?.isThinker) {
176
- setModel(MODELS[0].value);
177
  }
178
  }
 
 
 
 
 
 
179
  }
180
  };
181
 
182
- const handleThink = (think: string) => {
183
- setThink(think);
184
- setIsThinking(true);
185
- setOpenThink(true);
186
- };
187
-
188
- const handleError = (error: string, message?: string) => {
189
- switch (error) {
190
- case "login_required":
191
- setOpen(true);
192
- break;
193
- case "provider_required":
194
- setOpenProvider(true);
195
- setProviderError(message || "");
196
- break;
197
- case "pro_required":
198
- setOpenProModal(true);
199
- break;
200
- case "api_error":
201
- toast.error(message || "An error occurred");
202
- break;
203
- case "network_error":
204
- toast.error(message || "Network error occurred");
205
- break;
206
- default:
207
- toast.error("An unexpected error occurred");
208
  }
209
  };
210
 
@@ -221,8 +287,8 @@ export function AskAI({
221
  }, [isThinking]);
222
 
223
  const isSameHtml = useMemo(() => {
224
- return isTheSameHtml(currentPage.html);
225
- }, [currentPage.html]);
226
 
227
  return (
228
  <div className="px-3">
@@ -264,13 +330,6 @@ export function AskAI({
264
  </main>
265
  </div>
266
  )}
267
- <SelectedFiles
268
- files={selectedFiles}
269
- isAiWorking={isAiWorking}
270
- onDelete={(file) =>
271
- setSelectedFiles((prev) => prev.filter((f) => f !== file))
272
- }
273
- />
274
  {selectedElement && (
275
  <div className="px-4 pt-3">
276
  <SelectedHtmlElement
@@ -281,29 +340,28 @@ export function AskAI({
281
  </div>
282
  )}
283
  <div className="w-full relative flex items-center justify-between">
284
- {(isAiWorking || isUploading) && (
285
- <div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
286
  <div className="flex items-center justify-start gap-2">
287
  <Loading overlay={false} className="!size-4" />
288
  <p className="text-neutral-400 text-sm">
289
- {isUploading ? "Uploading images..." : "AI is working..."}
290
  </p>
291
  </div>
292
- {isAiWorking && (
293
- <div
294
- className="text-xs text-neutral-400 px-1 py-0.5 rounded-md border border-neutral-600 flex items-center justify-center gap-1.5 bg-neutral-800 hover:brightness-110 transition-all duration-200 cursor-pointer"
295
- onClick={stopController}
296
- >
297
- <FaStopCircle />
298
- Stop generation
299
- </div>
300
- )}
301
  </div>
302
  )}
303
- <textarea
 
304
  disabled={isAiWorking}
305
  className={classNames(
306
- "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
307
  {
308
  "!pt-2.5": selectedElement && !isAiWorking,
309
  }
@@ -311,7 +369,7 @@ export function AskAI({
311
  placeholder={
312
  selectedElement
313
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
314
- : isFollowUp && (!isSameHtml || pages?.length > 1)
315
  ? "Ask DeepSite for edits"
316
  : "Ask DeepSite anything..."
317
  }
@@ -324,25 +382,9 @@ export function AskAI({
324
  }}
325
  />
326
  </div>
327
- <div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
328
  <div className="flex-1 flex items-center justify-start gap-1.5">
329
- <Uploader
330
- pages={pages}
331
- onLoading={setIsUploading}
332
- isLoading={isUploading}
333
- onFiles={setFiles}
334
- onSelectFile={(file) => {
335
- if (selectedFiles.includes(file)) {
336
- setSelectedFiles((prev) => prev.filter((f) => f !== file));
337
- } else {
338
- setSelectedFiles((prev) => [...prev, file]);
339
- }
340
- }}
341
- files={files}
342
- selectedFiles={selectedFiles}
343
- project={project}
344
- />
345
- {isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
346
  {!isSameHtml && (
347
  <Tooltip>
348
  <TooltipTrigger asChild>
@@ -370,7 +412,7 @@ export function AskAI({
370
  </TooltipContent>
371
  </Tooltip>
372
  )}
373
- {/* <InviteFriends /> */}
374
  </div>
375
  <div className="flex items-center justify-end gap-2">
376
  <Settings
@@ -392,22 +434,12 @@ export function AskAI({
392
  </Button>
393
  </div>
394
  </div>
395
- <LoginModal open={open} onClose={() => setOpen(false)} pages={pages} />
396
  <ProModal
397
- pages={pages}
398
  open={openProModal}
399
  onClose={() => setOpenProModal(false)}
400
  />
401
- {pages.length === 1 && (
402
- <div className="border border-sky-500/20 bg-sky-500/40 hover:bg-sky-600 transition-all duration-200 text-sky-500 pl-2 pr-4 py-1.5 text-xs rounded-full absolute top-0 -translate-y-[calc(100%+8px)] left-0 max-w-max flex items-center justify-start gap-2">
403
- <span className="rounded-full text-[10px] font-semibold bg-white text-neutral-900 px-1.5 py-0.5">
404
- NEW
405
- </span>
406
- <p className="text-sm text-neutral-100">
407
- DeepSite can now create multiple pages at once. Try it!
408
- </p>
409
- </div>
410
- )}
411
  {!isSameHtml && (
412
  <div className="absolute top-0 right-0 -translate-y-[calc(100%+8px)] select-none text-xs text-neutral-400 flex items-center justify-center gap-2 bg-neutral-800 border border-neutral-700 rounded-md p-1 pr-2.5">
413
  <label
@@ -430,7 +462,7 @@ export function AskAI({
430
  </div>
431
  )}
432
  </div>
433
- <audio ref={hookAudio} id="audio" className="hidden">
434
  <source src="/success.mp3" type="audio/mpeg" />
435
  Your browser does not support the audio element.
436
  </audio>
 
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
  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";
17
  import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
 
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);
 
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
  }, [isThinking]);
288
 
289
  const isSameHtml = useMemo(() => {
290
+ return isTheSameHtml(html);
291
+ }, [html]);
292
 
293
  return (
294
  <div className="px-3">
 
330
  </main>
331
  </div>
332
  )}
 
 
 
 
 
 
 
333
  {selectedElement && (
334
  <div className="px-4 pt-3">
335
  <SelectedHtmlElement
 
340
  </div>
341
  )}
342
  <div className="w-full relative flex items-center justify-between">
343
+ {isAiWorking && (
344
+ <div className="absolute bg-neutral-800 rounded-lg bottom-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-center justify-between max-lg:text-sm">
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
352
+ className="text-xs text-neutral-400 px-1 py-0.5 rounded-md border border-neutral-600 flex items-center justify-center gap-1.5 bg-neutral-800 hover:brightness-110 transition-all duration-200 cursor-pointer"
353
+ onClick={stopController}
354
+ >
355
+ <FaStopCircle />
356
+ Stop generation
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
  placeholder={
370
  selectedElement
371
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
372
+ : hasAsked
373
  ? "Ask DeepSite for edits"
374
  : "Ask DeepSite anything..."
375
  }
 
382
  }}
383
  />
384
  </div>
385
+ <div className="flex items-center justify-between gap-2 px-4 pb-3">
386
  <div className="flex-1 flex items-center justify-start gap-1.5">
387
+ <ReImagine onRedesign={(md) => callAi(md)} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  {!isSameHtml && (
389
  <Tooltip>
390
  <TooltipTrigger asChild>
 
412
  </TooltipContent>
413
  </Tooltip>
414
  )}
415
+ <InviteFriends />
416
  </div>
417
  <div className="flex items-center justify-end gap-2">
418
  <Settings
 
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
  />
 
 
 
 
 
 
 
 
 
 
443
  {!isSameHtml && (
444
  <div className="absolute top-0 right-0 -translate-y-[calc(100%+8px)] select-none text-xs text-neutral-400 flex items-center justify-center gap-2 bg-neutral-800 border border-neutral-700 rounded-md p-1 pr-2.5">
445
  <label
 
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>
components/editor/ask-ai/selected-files.tsx DELETED
@@ -1,47 +0,0 @@
1
- import Image from "next/image";
2
-
3
- import { Button } from "@/components/ui/button";
4
- import { Minus } from "lucide-react";
5
-
6
- export const SelectedFiles = ({
7
- files,
8
- isAiWorking,
9
- onDelete,
10
- }: {
11
- files: string[];
12
- isAiWorking: boolean;
13
- onDelete: (file: string) => void;
14
- }) => {
15
- if (files.length === 0) return null;
16
- return (
17
- <div className="px-4 pt-3">
18
- <div className="flex items-center justify-start gap-2">
19
- {files.map((file) => (
20
- <div
21
- key={file}
22
- className="flex items-center relative justify-start gap-2 p-1 bg-neutral-700 rounded-md"
23
- >
24
- <Image
25
- src={file}
26
- alt="uploaded image"
27
- className="size-12 rounded-md object-cover"
28
- width={40}
29
- height={40}
30
- />
31
- <Button
32
- size="iconXsss"
33
- variant="secondary"
34
- className={`absolute top-0.5 right-0.5 ${
35
- isAiWorking ? "opacity-50 !cursor-not-allowed" : ""
36
- }`}
37
- disabled={isAiWorking}
38
- onClick={() => onDelete(file)}
39
- >
40
- <Minus className="size-4" />
41
- </Button>
42
- </div>
43
- ))}
44
- </div>
45
- </div>
46
- );
47
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/ask-ai/settings.tsx CHANGED
@@ -80,14 +80,16 @@ export function Settings({
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,
 
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,
components/editor/ask-ai/uploader.tsx DELETED
@@ -1,203 +0,0 @@
1
- import { useRef, useState } from "react";
2
- import { Images, Upload } from "lucide-react";
3
- import Image from "next/image";
4
-
5
- import {
6
- Popover,
7
- PopoverContent,
8
- PopoverTrigger,
9
- } from "@/components/ui/popover";
10
- import { Button } from "@/components/ui/button";
11
- import { Page, Project } from "@/types";
12
- import Loading from "@/components/loading";
13
- import { RiCheckboxCircleFill } from "react-icons/ri";
14
- import { useUser } from "@/hooks/useUser";
15
- import { LoginModal } from "@/components/login-modal";
16
- import { DeployButtonContent } from "../deploy-button/content";
17
-
18
- export const Uploader = ({
19
- pages,
20
- onLoading,
21
- isLoading,
22
- onFiles,
23
- onSelectFile,
24
- selectedFiles,
25
- files,
26
- project,
27
- }: {
28
- pages: Page[];
29
- onLoading: (isLoading: boolean) => void;
30
- isLoading: boolean;
31
- files: string[];
32
- onFiles: React.Dispatch<React.SetStateAction<string[]>>;
33
- onSelectFile: (file: string) => void;
34
- selectedFiles: string[];
35
- project?: Project | null;
36
- }) => {
37
- const { user } = useUser();
38
-
39
- const [open, setOpen] = useState(false);
40
- const fileInputRef = useRef<HTMLInputElement>(null);
41
-
42
- const uploadFiles = async (files: FileList | null) => {
43
- if (!files) return;
44
- if (!project) return;
45
-
46
- onLoading(true);
47
-
48
- const images = Array.from(files).filter((file) => {
49
- return file.type.startsWith("image/");
50
- });
51
-
52
- const data = new FormData();
53
- images.forEach((image) => {
54
- data.append("images", image);
55
- });
56
-
57
- const response = await fetch(
58
- `/api/me/projects/${project.space_id}/images`,
59
- {
60
- method: "POST",
61
- body: data,
62
- }
63
- );
64
- if (response.ok) {
65
- const data = await response.json();
66
- onFiles((prev) => [...prev, ...data.uploadedFiles]);
67
- }
68
- onLoading(false);
69
- };
70
-
71
- // TODO FIRST PUBLISH YOUR PROJECT TO UPLOAD IMAGES.
72
- return user?.id ? (
73
- <Popover open={open} onOpenChange={setOpen}>
74
- <form>
75
- <PopoverTrigger asChild>
76
- <Button
77
- size="iconXs"
78
- variant="outline"
79
- className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
80
- >
81
- <Images className="size-4" />
82
- </Button>
83
- </PopoverTrigger>
84
- <PopoverContent
85
- align="start"
86
- className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
87
- >
88
- {project?.space_id ? (
89
- <>
90
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
91
- <div className="flex items-center justify-center -space-x-4 mb-3">
92
- <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
93
- 🎨
94
- </div>
95
- <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
96
- 🖼️
97
- </div>
98
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
99
- 💻
100
- </div>
101
- </div>
102
- <p className="text-xl font-semibold text-neutral-950">
103
- Add Custom Images
104
- </p>
105
- <p className="text-sm text-neutral-500 mt-1.5">
106
- Upload images to your project and use them with DeepSite!
107
- </p>
108
- </header>
109
- <main className="space-y-4 p-5">
110
- <div>
111
- <p className="text-xs text-left text-neutral-700 mb-2">
112
- Uploaded Images
113
- </p>
114
- <div className="grid grid-cols-4 gap-1 flex-wrap max-h-40 overflow-y-auto">
115
- {files.map((file) => (
116
- <div
117
- key={file}
118
- className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
119
- onClick={() => onSelectFile(file)}
120
- >
121
- <Image
122
- src={file}
123
- alt="uploaded image"
124
- width={56}
125
- height={56}
126
- className="object-cover w-full rounded-sm aspect-square"
127
- />
128
- {selectedFiles.includes(file) && (
129
- <div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
130
- <RiCheckboxCircleFill className="size-6 text-neutral-100" />
131
- </div>
132
- )}
133
- </div>
134
- ))}
135
- </div>
136
- </div>
137
- <div>
138
- <p className="text-xs text-left text-neutral-700 mb-2">
139
- Or import images from your computer
140
- </p>
141
- <Button
142
- variant="black"
143
- onClick={() => fileInputRef.current?.click()}
144
- className="relative w-full"
145
- >
146
- {isLoading ? (
147
- <>
148
- <Loading
149
- overlay={false}
150
- className="ml-2 size-4 animate-spin"
151
- />
152
- Uploading image(s)...
153
- </>
154
- ) : (
155
- <>
156
- <Upload className="size-4" />
157
- Upload Images
158
- </>
159
- )}
160
- </Button>
161
- <input
162
- ref={fileInputRef}
163
- type="file"
164
- className="hidden"
165
- multiple
166
- accept="image/*"
167
- onChange={(e) => uploadFiles(e.target.files)}
168
- />
169
- </div>
170
- </main>
171
- </>
172
- ) : (
173
- <DeployButtonContent
174
- pages={pages}
175
- prompts={[]}
176
- options={{
177
- description: "Publish your project first to add custom images.",
178
- }}
179
- />
180
- )}
181
- </PopoverContent>
182
- </form>
183
- </Popover>
184
- ) : (
185
- <>
186
- <Button
187
- size="iconXs"
188
- variant="outline"
189
- className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
190
- onClick={() => setOpen(true)}
191
- >
192
- <Images className="size-4" />
193
- </Button>
194
- <LoginModal
195
- open={open}
196
- onClose={() => setOpen(false)}
197
- pages={pages}
198
- title="Log In to add Custom Images"
199
- description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
200
- />
201
- </>
202
- );
203
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/deploy-button/content.tsx DELETED
@@ -1,111 +0,0 @@
1
- import { Rocket } from "lucide-react";
2
- import Image from "next/image";
3
-
4
- import Loading from "@/components/loading";
5
- import { Button } from "@/components/ui/button";
6
- import { Input } from "@/components/ui/input";
7
- import SpaceIcon from "@/assets/space.svg";
8
- import { Page } from "@/types";
9
- import { api } from "@/lib/api";
10
- import { toast } from "sonner";
11
- import { useState } from "react";
12
- import { useRouter } from "next/navigation";
13
-
14
- export const DeployButtonContent = ({
15
- pages,
16
- options,
17
- prompts,
18
- }: {
19
- pages: Page[];
20
- options?: {
21
- title?: string;
22
- description?: string;
23
- };
24
- prompts: string[];
25
- }) => {
26
- const router = useRouter();
27
- const [loading, setLoading] = useState(false);
28
-
29
- const [config, setConfig] = useState({
30
- title: "",
31
- });
32
-
33
- const createSpace = async () => {
34
- if (!config.title) {
35
- toast.error("Please enter a title for your space.");
36
- return;
37
- }
38
- setLoading(true);
39
-
40
- try {
41
- const res = await api.post("/me/projects", {
42
- title: config.title,
43
- pages,
44
- prompts,
45
- });
46
- if (res.data.ok) {
47
- router.push(`/projects/${res.data.path}?deploy=true`);
48
- } else {
49
- toast.error(res?.data?.error || "Failed to create space");
50
- }
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- } catch (err: any) {
53
- toast.error(err.response?.data?.error || err.message);
54
- } finally {
55
- setLoading(false);
56
- }
57
- };
58
-
59
- return (
60
- <>
61
- <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
62
- <div className="flex items-center justify-center -space-x-4 mb-3">
63
- <div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
64
- 🚀
65
- </div>
66
- <div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
67
- <Image src={SpaceIcon} alt="Space Icon" className="size-7" />
68
- </div>
69
- <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
70
- 👻
71
- </div>
72
- </div>
73
- <p className="text-xl font-semibold text-neutral-950">
74
- Publish as Space!
75
- </p>
76
- <p className="text-sm text-neutral-500 mt-1.5">
77
- {options?.description ??
78
- "Save and Publish your project to a Space on the Hub. Spaces are a way to share your project with the world."}
79
- </p>
80
- </header>
81
- <main className="space-y-4 p-6">
82
- <div>
83
- <p className="text-sm text-neutral-700 mb-2">
84
- Choose a title for your space:
85
- </p>
86
- <Input
87
- type="text"
88
- placeholder="My Awesome Website"
89
- value={config.title}
90
- onChange={(e) => setConfig({ ...config, title: e.target.value })}
91
- className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
92
- />
93
- </div>
94
- <div>
95
- <p className="text-sm text-neutral-700 mb-2">
96
- Then, let&apos;s publish it!
97
- </p>
98
- <Button
99
- variant="black"
100
- onClick={createSpace}
101
- className="relative w-full"
102
- disabled={loading}
103
- >
104
- Publish Space <Rocket className="size-4" />
105
- {loading && <Loading className="ml-2 size-4 animate-spin" />}
106
- </Button>
107
- </div>
108
- </main>
109
- </>
110
- );
111
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/deploy-button/index.tsx CHANGED
@@ -1,28 +1,67 @@
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
  import { useState } from "react";
 
 
 
3
  import { MdSave } from "react-icons/md";
 
4
 
 
 
5
  import { Button } from "@/components/ui/button";
6
  import {
7
  Popover,
8
  PopoverContent,
9
  PopoverTrigger,
10
  } from "@/components/ui/popover";
 
 
11
  import { LoginModal } from "@/components/login-modal";
12
  import { useUser } from "@/hooks/useUser";
13
- import { Page } from "@/types";
14
- import { DeployButtonContent } from "./content";
15
 
16
  export function DeployButton({
17
- pages,
18
  prompts,
19
  }: {
20
- pages: Page[];
21
  prompts: string[];
22
  }) {
 
23
  const { user } = useUser();
 
24
  const [open, setOpen] = useState(false);
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  return (
27
  <div className="flex items-center justify-end gap-5">
28
  <div className="relative flex items-center justify-end">
@@ -32,10 +71,10 @@ export function DeployButton({
32
  <div>
33
  <Button variant="default" className="max-lg:hidden !px-4">
34
  <MdSave className="size-4" />
35
- Publish your Project
36
  </Button>
37
  <Button variant="default" size="sm" className="lg:hidden">
38
- Publish
39
  </Button>
40
  </div>
41
  </PopoverTrigger>
@@ -43,7 +82,62 @@ export function DeployButton({
43
  className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
44
  align="end"
45
  >
46
- <DeployButtonContent pages={pages} prompts={prompts} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </PopoverContent>
48
  </Popover>
49
  ) : (
@@ -54,7 +148,7 @@ export function DeployButton({
54
  onClick={() => setOpen(true)}
55
  >
56
  <MdSave className="size-4" />
57
- Publish your Project
58
  </Button>
59
  <Button
60
  variant="default"
@@ -62,16 +156,16 @@ export function DeployButton({
62
  className="lg:hidden"
63
  onClick={() => setOpen(true)}
64
  >
65
- Publish
66
  </Button>
67
  </>
68
  )}
69
  <LoginModal
70
  open={open}
71
  onClose={() => setOpen(false)}
72
- pages={pages}
73
- title="Log In to publish your Project"
74
- description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
75
  />
76
  </div>
77
  </div>
 
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
  import { useState } from "react";
3
+ import { toast } from "sonner";
4
+ import Image from "next/image";
5
+ import { useRouter } from "next/navigation";
6
  import { MdSave } from "react-icons/md";
7
+ import { Rocket } from "lucide-react";
8
 
9
+ import SpaceIcon from "@/assets/space.svg";
10
+ import Loading from "@/components/loading";
11
  import { Button } from "@/components/ui/button";
12
  import {
13
  Popover,
14
  PopoverContent,
15
  PopoverTrigger,
16
  } from "@/components/ui/popover";
17
+ 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();
30
  const { user } = useUser();
31
+ const [loading, setLoading] = useState(false);
32
  const [open, setOpen] = useState(false);
33
 
34
+ const [config, setConfig] = useState({
35
+ title: "",
36
+ });
37
+
38
+ const createSpace = async () => {
39
+ if (!config.title) {
40
+ toast.error("Please enter a title for your space.");
41
+ return;
42
+ }
43
+ setLoading(true);
44
+
45
+ try {
46
+ const res = await api.post("/me/projects", {
47
+ title: config.title,
48
+ html,
49
+ prompts,
50
+ });
51
+ if (res.data.ok) {
52
+ router.push(`/projects/${res.data.path}?deploy=true`);
53
+ } else {
54
+ toast.error(res?.data?.error || "Failed to create space");
55
+ }
56
+ } catch (err: any) {
57
+ toast.error(err.response?.data?.error || err.message);
58
+ } finally {
59
+ setLoading(false);
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
  <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>
 
82
  className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
83
  align="end"
84
  >
85
+ <header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
86
+ <div className="flex items-center justify-center -space-x-4 mb-3">
87
+ <div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
88
+ 🚀
89
+ </div>
90
+ <div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
91
+ <Image
92
+ src={SpaceIcon}
93
+ alt="Space Icon"
94
+ className="size-7"
95
+ />
96
+ </div>
97
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
98
+ 👻
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">
110
+ <div>
111
+ <p className="text-sm text-neutral-700 mb-2">
112
+ Choose a title for your space:
113
+ </p>
114
+ <Input
115
+ type="text"
116
+ placeholder="My Awesome Website"
117
+ value={config.title}
118
+ onChange={(e) =>
119
+ setConfig({ ...config, title: e.target.value })
120
+ }
121
+ className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
122
+ />
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"
130
+ onClick={createSpace}
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
+ )}
138
+ </Button>
139
+ </div>
140
+ </main>
141
  </PopoverContent>
142
  </Popover>
143
  ) : (
 
148
  onClick={() => setOpen(true)}
149
  >
150
  <MdSave className="size-4" />
151
+ Save your Project
152
  </Button>
153
  <Button
154
  variant="default"
 
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>
components/editor/footer/index.tsx CHANGED
@@ -1,16 +1,13 @@
1
  import classNames from "classnames";
2
  import { FaMobileAlt } from "react-icons/fa";
3
- import { HelpCircle, LogIn, 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";
9
  import { UserMenu } from "@/components/user-menu";
10
  import { useUser } from "@/hooks/useUser";
11
- import Link from "next/link";
12
- import { useLocalStorage } from "react-use";
13
- import { isTheSameHtml } from "@/lib/compare-html-diff";
14
 
15
  const DEVICES = [
16
  {
@@ -24,23 +21,21 @@ const DEVICES = [
24
  ];
25
 
26
  export function Footer({
27
- pages,
28
- isNew = false,
29
  htmlHistory,
30
- setPages,
31
  device,
32
  setDevice,
33
  iframeRef,
34
  }: {
35
- pages: Page[];
36
- isNew?: boolean;
37
  htmlHistory?: HtmlHistory[];
38
  device: "desktop" | "mobile";
39
- setPages: (pages: Page[]) => void;
40
  iframeRef?: React.RefObject<HTMLIFrameElement | null>;
41
  setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
42
  }) {
43
- const { user, openLoginWindow } = useUser();
44
 
45
  const handleRefreshIframe = () => {
46
  if (iframeRef?.current) {
@@ -53,19 +48,11 @@ export function Footer({
53
  }
54
  };
55
 
56
- const [, setStorage] = useLocalStorage("pages");
57
- const handleClick = async () => {
58
- if (pages && !isTheSameHtml(pages[0].html)) {
59
- setStorage(pages);
60
- }
61
- openLoginWindow();
62
- };
63
-
64
  return (
65
  <footer className="border-t bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 py-2 flex items-center justify-between sticky bottom-0 z-20">
66
  <div className="flex items-center gap-2">
67
- {user ? (
68
- user?.isLocalUse ? (
69
  <>
70
  <div className="max-w-max bg-amber-500/10 rounded-full px-3 py-1 text-amber-500 border border-amber-500/20 text-sm font-semibold">
71
  Local Usage
@@ -73,26 +60,16 @@ export function Footer({
73
  </>
74
  ) : (
75
  <UserMenu className="!p-1 !pr-3 !h-auto" />
76
- )
77
- ) : (
78
- <Button size="sm" variant="default" onClick={handleClick}>
79
- <LogIn className="text-sm" />
80
- Log In
81
- </Button>
82
- )}
83
- {user && !isNew && <p className="text-neutral-700">|</p>}
84
- {!isNew && (
85
- <Link href="/projects/new">
86
- <Button size="sm" variant="secondary">
87
- <MdAdd className="text-sm" />
88
- New <span className="max-lg:hidden">Project</span>
89
- </Button>
90
- </Link>
91
- )}
92
  {htmlHistory && htmlHistory.length > 0 && (
93
  <>
94
  <p className="text-neutral-700">|</p>
95
- <History history={htmlHistory} setPages={setPages} />
96
  </>
97
  )}
98
  </div>
 
1
  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";
9
  import { UserMenu } from "@/components/user-menu";
10
  import { useUser } from "@/hooks/useUser";
 
 
 
11
 
12
  const DEVICES = [
13
  {
 
21
  ];
22
 
23
  export function Footer({
24
+ onReset,
 
25
  htmlHistory,
26
+ setHtml,
27
  device,
28
  setDevice,
29
  iframeRef,
30
  }: {
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
  }) {
38
+ const { user } = useUser();
39
 
40
  const handleRefreshIframe = () => {
41
  if (iframeRef?.current) {
 
48
  }
49
  };
50
 
 
 
 
 
 
 
 
 
51
  return (
52
  <footer className="border-t bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 py-2 flex items-center justify-between sticky bottom-0 z-20">
53
  <div className="flex items-center gap-2">
54
+ {user &&
55
+ (user?.isLocalUse ? (
56
  <>
57
  <div className="max-w-max bg-amber-500/10 rounded-full px-3 py-1 text-amber-500 border border-amber-500/20 text-sm font-semibold">
58
  Local Usage
 
60
  </>
61
  ) : (
62
  <UserMenu className="!p-1 !pr-3 !h-auto" />
63
+ ))}
64
+ {user && <p className="text-neutral-700">|</p>}
65
+ <Button size="sm" variant="secondary" onClick={onReset}>
66
+ <MdAdd className="text-sm" />
67
+ New <span className="max-lg:hidden">Project</span>
68
+ </Button>
 
 
 
 
 
 
 
 
 
 
69
  {htmlHistory && htmlHistory.length > 0 && (
70
  <>
71
  <p className="text-neutral-700">|</p>
72
+ <History history={htmlHistory} setHtml={setHtml} />
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, Page } 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
- setPages,
13
  }: {
14
  history: HtmlHistory[];
15
- setPages: (pages: Page[]) => void;
16
  }) {
17
  return (
18
  <Popover>
@@ -57,8 +57,7 @@ export function History({
57
  variant="sky"
58
  size="xs"
59
  onClick={() => {
60
- console.log(item);
61
- setPages(item.pages);
62
  }}
63
  >
64
  Select
 
1
  import { History as HistoryIcon } from "lucide-react";
2
+ import { HtmlHistory } from "@/types";
3
  import {
4
  Popover,
5
  PopoverContent,
 
9
 
10
  export function History({
11
  history,
12
+ setHtml,
13
  }: {
14
  history: HtmlHistory[];
15
+ setHtml: (html: string) => void;
16
  }) {
17
  return (
18
  <Popover>
 
57
  variant="sky"
58
  size="xs"
59
  onClick={() => {
60
+ setHtml(item.html);
 
61
  }}
62
  >
63
  Select
components/editor/index.tsx CHANGED
@@ -1,5 +1,5 @@
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,32 +22,17 @@ 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 { 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
- images,
35
- isNew,
36
- }: {
37
- project?: Project | null;
38
- pages?: Page[];
39
- images?: string[];
40
- isNew?: boolean;
41
- }) => {
42
- const [htmlStorage, , removeHtmlStorage] = useLocalStorage("pages");
43
  const [, copyToClipboard] = useCopyToClipboard();
44
- const { htmlHistory, setHtmlHistory, prompts, setPrompts, pages, setPages } =
45
- useEditor(
46
- initialPages,
47
- project?.prompts ?? [],
48
- typeof htmlStorage === "string" ? htmlStorage : undefined
49
- );
50
-
51
  const searchParams = useSearchParams();
52
  const router = useRouter();
53
  const deploy = searchParams.get("deploy") === "true";
@@ -61,7 +46,6 @@ export const AppEditor = ({
61
  const monacoRef = useRef<any>(null);
62
 
63
  const [currentTab, setCurrentTab] = useState("chat");
64
- const [currentPage, setCurrentPage] = useState("index.html");
65
  const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
66
  const [isResizing, setIsResizing] = useState(false);
67
  const [isAiWorking, setIsAiWorking] = useState(false);
@@ -69,8 +53,12 @@ export const AppEditor = ({
69
  const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
70
  null
71
  );
72
- const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
73
 
 
 
 
 
 
74
  const resetLayout = () => {
75
  if (!editor.current || !preview.current) return;
76
 
@@ -90,6 +78,10 @@ export const AppEditor = ({
90
  }
91
  };
92
 
 
 
 
 
93
  const handleResize = (e: MouseEvent) => {
94
  if (!editor.current || !preview.current || !resizer.current) return;
95
 
@@ -157,7 +149,7 @@ export const AppEditor = ({
157
 
158
  // Prevent accidental navigation away when AI is working or content has changed
159
  useEvent("beforeunload", (e) => {
160
- if (isAiWorking || !isTheSameHtml(currentPageData?.html)) {
161
  e.preventDefault();
162
  return "";
163
  }
@@ -183,15 +175,6 @@ export const AppEditor = ({
183
  console.log("Editor validation markers:", markers);
184
  };
185
 
186
- const currentPageData = useMemo(() => {
187
- return (
188
- pages.find((page) => page.path === currentPage) ?? {
189
- path: "index.html",
190
- html: defaultHTML,
191
- }
192
- );
193
- }, [pages, currentPage]);
194
-
195
  return (
196
  <section className="h-[100dvh] bg-neutral-950 flex flex-col">
197
  <Header tab={currentTab} onNewTab={setCurrentTab}>
@@ -200,11 +183,10 @@ export const AppEditor = ({
200
  router.push(`/projects/${project.space_id}`);
201
  }}
202
  />
203
- {/* for these buttons pass the whole pages */}
204
  {project?._id ? (
205
- <SaveButton pages={pages} prompts={prompts} />
206
  ) : (
207
- <DeployButton pages={pages} prompts={prompts} />
208
  )}
209
  </Header>
210
  <main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full max-lg:h-[calc(100%-82px)] relative">
@@ -214,43 +196,10 @@ export const AppEditor = ({
214
  ref={editor}
215
  className="bg-neutral-900 relative flex-1 overflow-hidden h-full flex flex-col gap-2 pb-3"
216
  >
217
- <ListPages
218
- pages={pages}
219
- currentPage={currentPage}
220
- onSelectPage={(path, newPath) => {
221
- if (newPath) {
222
- setPages((prev) =>
223
- prev.map((page) =>
224
- page.path === path ? { ...page, path: newPath } : page
225
- )
226
- );
227
- setCurrentPage(newPath);
228
- } else {
229
- setCurrentPage(path);
230
- }
231
- }}
232
- onDeletePage={(path) => {
233
- const newPages = pages.filter((page) => page.path !== path);
234
- setPages(newPages);
235
- if (currentPage === path) {
236
- setCurrentPage(newPages[0]?.path ?? "index.html");
237
- }
238
- }}
239
- onNewPage={() => {
240
- setPages((prev) => [
241
- ...prev,
242
- {
243
- path: `page-${prev.length + 1}.html`,
244
- html: defaultHTML,
245
- },
246
- ]);
247
- setCurrentPage(`page-${pages.length + 1}.html`);
248
- }}
249
- />
250
  <CopyIcon
251
- className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
252
  onClick={() => {
253
- copyToClipboard(currentPageData.html);
254
  toast.success("HTML copied to clipboard!");
255
  }}
256
  />
@@ -273,17 +222,10 @@ export const AppEditor = ({
273
  },
274
  wordWrap: "on",
275
  }}
276
- value={currentPageData.html}
277
  onChange={(value) => {
278
  const newValue = value ?? "";
279
- // setHtml(newValue);
280
- setPages((prev) =>
281
- prev.map((page) =>
282
- page.path === currentPageData.path
283
- ? { ...page, html: newValue }
284
- : page
285
- )
286
- );
287
  }}
288
  onMount={(editor, monaco) => {
289
  editorRef.current = editor;
@@ -292,49 +234,49 @@ export const AppEditor = ({
292
  onValidate={handleEditorValidation}
293
  />
294
  <AskAI
295
- project={project}
296
- images={images}
297
- currentPage={currentPageData}
 
298
  htmlHistory={htmlHistory}
299
- previousPrompts={prompts}
300
- onSuccess={(newPages, p: string) => {
 
 
 
301
  const currentHistory = [...htmlHistory];
302
  currentHistory.unshift({
303
- pages: newPages,
304
  createdAt: new Date(),
305
  prompt: p,
306
  });
307
  setHtmlHistory(currentHistory);
308
  setSelectedElement(null);
309
- setSelectedFiles([]);
310
  // if xs or sm
311
  if (window.innerWidth <= 1024) {
312
  setCurrentTab("preview");
313
  }
314
- // if (updatedLines && updatedLines?.length > 0) {
315
- // const decorations = updatedLines.map((line) => ({
316
- // range: new monacoRef.current.Range(
317
- // line[0],
318
- // 1,
319
- // line[1],
320
- // 1
321
- // ),
322
- // options: {
323
- // inlineClassName: "matched-line",
324
- // },
325
- // }));
326
- // setTimeout(() => {
327
- // editorRef?.current
328
- // ?.getModel()
329
- // ?.deltaDecorations([], decorations);
330
 
331
- // editorRef.current?.revealLine(updatedLines[0][0]);
332
- // }, 100);
333
- // }
334
  }}
335
- setPages={setPages}
336
- pages={pages}
337
- setCurrentPage={setCurrentPage}
338
  isAiWorking={isAiWorking}
339
  setisAiWorking={setIsAiWorking}
340
  onNewPrompt={(prompt: string) => {
@@ -345,13 +287,10 @@ export const AppEditor = ({
345
  editorRef.current?.getModel()?.getLineCount() ?? 0
346
  );
347
  }}
348
- isNew={isNew}
349
  isEditableModeEnabled={isEditableModeEnabled}
350
  setIsEditableModeEnabled={setIsEditableModeEnabled}
351
  selectedElement={selectedElement}
352
  setSelectedElement={setSelectedElement}
353
- setSelectedFiles={setSelectedFiles}
354
- selectedFiles={selectedFiles}
355
  />
356
  </div>
357
  <div
@@ -361,13 +300,11 @@ export const AppEditor = ({
361
  </>
362
  )}
363
  <Preview
364
- html={currentPageData?.html}
365
  isResizing={isResizing}
366
  isAiWorking={isAiWorking}
367
  ref={preview}
368
  device={device}
369
- pages={pages}
370
- setCurrentPage={setCurrentPage}
371
  currentTab={currentTab}
372
  isEditableModeEnabled={isEditableModeEnabled}
373
  iframeRef={iframeRef}
@@ -379,12 +316,25 @@ export const AppEditor = ({
379
  />
380
  </main>
381
  <Footer
382
- pages={pages}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  htmlHistory={htmlHistory}
384
- setPages={setPages}
385
  iframeRef={iframeRef}
386
  device={device}
387
- isNew={isNew}
388
  setDevice={setDevice}
389
  />
390
  </section>
 
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
  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();
38
  const deploy = searchParams.get("deploy") === "true";
 
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);
 
53
  const [selectedElement, setSelectedElement] = useState<HTMLElement | 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
  }
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
 
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
  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
  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
  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
  },
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
  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
  });
253
  setHtmlHistory(currentHistory);
254
  setSelectedElement(null);
 
255
  // if xs or sm
256
  if (window.innerWidth <= 1024) {
257
  setCurrentTab("preview");
258
  }
259
+ if (updatedLines && updatedLines?.length > 0) {
260
+ const decorations = updatedLines.map((line) => ({
261
+ range: new monacoRef.current.Range(
262
+ line[0],
263
+ 1,
264
+ line[1],
265
+ 1
266
+ ),
267
+ options: {
268
+ inlineClassName: "matched-line",
269
+ },
270
+ }));
271
+ setTimeout(() => {
272
+ editorRef?.current
273
+ ?.getModel()
274
+ ?.deltaDecorations([], decorations);
275
 
276
+ editorRef.current?.revealLine(updatedLines[0][0]);
277
+ }, 100);
278
+ }
279
  }}
 
 
 
280
  isAiWorking={isAiWorking}
281
  setisAiWorking={setIsAiWorking}
282
  onNewPrompt={(prompt: string) => {
 
287
  editorRef.current?.getModel()?.getLineCount() ?? 0
288
  );
289
  }}
 
290
  isEditableModeEnabled={isEditableModeEnabled}
291
  setIsEditableModeEnabled={setIsEditableModeEnabled}
292
  selectedElement={selectedElement}
293
  setSelectedElement={setSelectedElement}
 
 
294
  />
295
  </div>
296
  <div
 
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}
 
316
  />
317
  </main>
318
  <Footer
319
+ onReset={() => {
320
+ if (isAiWorking) {
321
+ toast.warning("Please wait for the AI to finish working.");
322
+ return;
323
+ }
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
331
+ );
332
+ }
333
+ }}
334
  htmlHistory={htmlHistory}
335
+ setHtml={setHtml}
336
  iframeRef={iframeRef}
337
  device={device}
 
338
  setDevice={setDevice}
339
  />
340
  </section>
components/editor/pages/index.tsx DELETED
@@ -1,30 +0,0 @@
1
- import { Page } from "@/types";
2
- import { ListPagesItem } from "./page";
3
-
4
- export function ListPages({
5
- pages,
6
- currentPage,
7
- onSelectPage,
8
- onDeletePage,
9
- }: {
10
- pages: Array<Page>;
11
- currentPage: string;
12
- onSelectPage: (path: string, newPath?: string) => void;
13
- onNewPage: () => void;
14
- onDeletePage: (path: string) => void;
15
- }) {
16
- return (
17
- <div className="w-full flex items-center justify-start bg-neutral-950 overflow-auto flex-nowrap min-h-[44px]">
18
- {pages.map((page, i) => (
19
- <ListPagesItem
20
- key={i}
21
- page={page}
22
- currentPage={currentPage}
23
- onSelectPage={onSelectPage}
24
- onDeletePage={onDeletePage}
25
- index={i}
26
- />
27
- ))}
28
- </div>
29
- );
30
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/editor/pages/page.tsx DELETED
@@ -1,82 +0,0 @@
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
@@ -3,12 +3,10 @@ import { useUpdateEffect } from "react-use";
3
  import { useMemo, useState } from "react";
4
  import classNames from "classnames";
5
  import { toast } from "sonner";
6
- import { useThrottleFn } from "react-use";
7
 
8
  import { cn } from "@/lib/utils";
9
  import { GridPattern } from "@/components/magic-ui/grid-pattern";
10
  import { htmlTagToText } from "@/lib/html-tag-to-text";
11
- import { Page } from "@/types";
12
 
13
  export const Preview = ({
14
  html,
@@ -18,16 +16,12 @@ export const Preview = ({
18
  device,
19
  currentTab,
20
  iframeRef,
21
- pages,
22
- setCurrentPage,
23
  isEditableModeEnabled,
24
  onClickElement,
25
  }: {
26
  html: string;
27
  isResizing: boolean;
28
  isAiWorking: boolean;
29
- pages: Page[];
30
- setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
31
  ref: React.RefObject<HTMLDivElement | null>;
32
  iframeRef?: React.RefObject<HTMLIFrameElement | null>;
33
  device: "desktop" | "mobile";
@@ -39,6 +33,7 @@ export const Preview = ({
39
  null
40
  );
41
 
 
42
  const handleMouseOver = (event: MouseEvent) => {
43
  if (iframeRef?.current) {
44
  const iframeDocument = iframeRef.current.contentDocument;
@@ -70,48 +65,6 @@ export const Preview = ({
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
- if (anchorElement) {
92
- let href = anchorElement.getAttribute("href");
93
- if (href) {
94
- event.stopPropagation();
95
- event.preventDefault();
96
-
97
- if (href.includes("#") && !href.includes(".html")) {
98
- const targetElement = iframeDocument.querySelector(href);
99
- if (targetElement) {
100
- targetElement.scrollIntoView({ behavior: "smooth" });
101
- }
102
- return;
103
- }
104
-
105
- href = href.split(".html")[0] + ".html";
106
- const isPageExist = pages.some((page) => page.path === href);
107
- if (isPageExist) {
108
- setCurrentPage(href);
109
- }
110
- }
111
- }
112
- }
113
- }
114
- };
115
 
116
  useUpdateEffect(() => {
117
  const cleanupListeners = () => {
@@ -126,6 +79,7 @@ export const Preview = ({
126
  if (iframeRef?.current) {
127
  const iframeDocument = iframeRef.current.contentDocument;
128
  if (iframeDocument) {
 
129
  cleanupListeners();
130
 
131
  if (isEditableModeEnabled) {
@@ -136,6 +90,7 @@ export const Preview = ({
136
  }
137
  }
138
 
 
139
  return cleanupListeners;
140
  }, [iframeRef, isEditableModeEnabled]);
141
 
@@ -145,8 +100,6 @@ export const Preview = ({
145
  return hoveredElement;
146
  }, [hoveredElement, isEditableModeEnabled]);
147
 
148
- const throttledHtml = useThrottleFn((html) => html, 1000, [html]);
149
-
150
  return (
151
  <div
152
  ref={ref}
@@ -207,7 +160,7 @@ export const Preview = ({
207
  currentTab !== "preview" && device === "desktop",
208
  }
209
  )}
210
- srcDoc={isAiWorking ? (throttledHtml as string) : html}
211
  onLoad={() => {
212
  if (iframeRef?.current?.contentWindow?.document?.body) {
213
  iframeRef.current.contentWindow.document.body.scrollIntoView({
@@ -216,14 +169,6 @@ export const Preview = ({
216
  behavior: isAiWorking ? "instant" : "smooth",
217
  });
218
  }
219
- // add event listener to all links in the iframe to handle navigation
220
- if (iframeRef?.current?.contentWindow?.document) {
221
- const links =
222
- iframeRef.current.contentWindow.document.querySelectorAll("a");
223
- links.forEach((link) => {
224
- link.addEventListener("click", handleCustomNavigation);
225
- });
226
- }
227
  }}
228
  />
229
  </div>
 
3
  import { useMemo, useState } from "react";
4
  import classNames from "classnames";
5
  import { toast } from "sonner";
 
6
 
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
  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";
 
33
  null
34
  );
35
 
36
+ // add event listener to the iframe to track hovered elements
37
  const handleMouseOver = (event: MouseEvent) => {
38
  if (iframeRef?.current) {
39
  const iframeDocument = iframeRef.current.contentDocument;
 
65
  }
66
  }
67
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  useUpdateEffect(() => {
70
  const cleanupListeners = () => {
 
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
  }
91
  }
92
 
93
+ // Clean up when component unmounts or dependencies change
94
  return cleanupListeners;
95
  }, [iframeRef, isEditableModeEnabled]);
96
 
 
100
  return hoveredElement;
101
  }, [hoveredElement, isEditableModeEnabled]);
102
 
 
 
103
  return (
104
  <div
105
  ref={ref}
 
160
  currentTab !== "preview" && device === "desktop",
161
  }
162
  )}
163
+ srcDoc={html}
164
  onLoad={() => {
165
  if (iframeRef?.current?.contentWindow?.document?.body) {
166
  iframeRef.current.contentWindow.document.body.scrollIntoView({
 
169
  behavior: isAiWorking ? "instant" : "smooth",
170
  });
171
  }
 
 
 
 
 
 
 
 
172
  }}
173
  />
174
  </div>
components/editor/save-button/index.tsx CHANGED
@@ -7,13 +7,12 @@ 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
- 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,7 +27,7 @@ export function SaveButton({
28
 
29
  try {
30
  const res = await api.put(`/me/projects/${namespace}/${repoId}`, {
31
- pages,
32
  prompts,
33
  });
34
  if (res.data.ok) {
@@ -60,7 +59,7 @@ export function SaveButton({
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,7 +68,7 @@ export function SaveButton({
69
  className="lg:hidden relative"
70
  onClick={updateSpace}
71
  >
72
- Publish {loading && <Loading className="ml-2 size-4 animate-spin" />}
73
  </Button>
74
  </>
75
  );
 
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
 
28
  try {
29
  const res = await api.put(`/me/projects/${namespace}/${repoId}`, {
30
+ html,
31
  prompts,
32
  });
33
  if (res.data.ok) {
 
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
  className="lg:hidden relative"
69
  onClick={updateSpace}
70
  >
71
+ Deploy {loading && <Loading className="ml-2 size-4 animate-spin" />}
72
  </Button>
73
  </>
74
  );
components/iframe-detector.tsx DELETED
@@ -1,75 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useState } from "react";
4
- import IframeWarningModal from "./iframe-warning-modal";
5
-
6
- export default function IframeDetector() {
7
- const [showWarning, setShowWarning] = useState(false);
8
-
9
- useEffect(() => {
10
- // Helper function to check if a hostname is from allowed domains
11
- const isAllowedDomain = (hostname: string) => {
12
- const host = hostname.toLowerCase();
13
- return (
14
- host.endsWith(".huggingface.co") ||
15
- host.endsWith(".hf.co") ||
16
- host === "huggingface.co" ||
17
- host === "hf.co"
18
- );
19
- };
20
-
21
- // Check if the current window is in an iframe
22
- const isInIframe = () => {
23
- try {
24
- return window.self !== window.top;
25
- } catch {
26
- // If we can't access window.top due to cross-origin restrictions,
27
- // we're likely in an iframe
28
- return true;
29
- }
30
- };
31
-
32
- // Additional check: compare window location with parent location
33
- const isEmbedded = () => {
34
- try {
35
- return window.location !== window.parent.location;
36
- } catch {
37
- // Cross-origin iframe
38
- return true;
39
- }
40
- };
41
-
42
- // Check if we're in an iframe from a non-allowed domain
43
- const shouldShowWarning = () => {
44
- if (!isInIframe() && !isEmbedded()) {
45
- return false; // Not in an iframe
46
- }
47
-
48
- try {
49
- // Try to get the parent's hostname
50
- const parentHostname = window.parent.location.hostname;
51
- return !isAllowedDomain(parentHostname);
52
- } catch {
53
- // Cross-origin iframe - try to get referrer instead
54
- try {
55
- if (document.referrer) {
56
- const referrerUrl = new URL(document.referrer);
57
- return !isAllowedDomain(referrerUrl.hostname);
58
- }
59
- } catch {
60
- // If we can't determine the parent domain, assume it's not allowed
61
- }
62
- return true;
63
- }
64
- };
65
-
66
- if (shouldShowWarning()) {
67
- // Show warning modal instead of redirecting immediately
68
- setShowWarning(true);
69
- }
70
- }, []);
71
-
72
- return (
73
- <IframeWarningModal isOpen={showWarning} onOpenChange={setShowWarning} />
74
- );
75
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/iframe-warning-modal.tsx DELETED
@@ -1,61 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- Dialog,
5
- DialogContent,
6
- DialogDescription,
7
- DialogFooter,
8
- DialogHeader,
9
- DialogTitle,
10
- } from "@/components/ui/dialog";
11
- import { Button } from "@/components/ui/button";
12
- import { ExternalLink, AlertTriangle } from "lucide-react";
13
-
14
- interface IframeWarningModalProps {
15
- isOpen: boolean;
16
- onOpenChange: (open: boolean) => void;
17
- }
18
-
19
- export default function IframeWarningModal({
20
- isOpen,
21
- }: // onOpenChange,
22
- IframeWarningModalProps) {
23
- const handleVisitSite = () => {
24
- window.open("https://deepsite.hf.co", "_blank");
25
- };
26
-
27
- return (
28
- <Dialog open={isOpen} onOpenChange={() => {}}>
29
- <DialogContent className="sm:max-w-md">
30
- <DialogHeader>
31
- <div className="flex items-center gap-2">
32
- <AlertTriangle className="h-5 w-5 text-red-500" />
33
- <DialogTitle>Unauthorized Embedding</DialogTitle>
34
- </div>
35
- <DialogDescription className="text-left">
36
- You&apos;re viewing DeepSite through an unauthorized iframe. For the
37
- best experience and security, please visit the official website
38
- directly.
39
- </DialogDescription>
40
- </DialogHeader>
41
-
42
- <div className="bg-muted/50 rounded-lg p-4 space-y-2">
43
- <p className="text-sm font-medium">Why visit the official site?</p>
44
- <ul className="text-sm text-muted-foreground space-y-1">
45
- <li>• Better performance and security</li>
46
- <li>• Full functionality access</li>
47
- <li>• Latest features and updates</li>
48
- <li>• Proper authentication support</li>
49
- </ul>
50
- </div>
51
-
52
- <DialogFooter className="flex-col sm:flex-row gap-2">
53
- <Button onClick={handleVisitSite} className="w-full sm:w-auto">
54
- <ExternalLink className="mr-2 h-4 w-4" />
55
- Visit Deepsite.hf.co
56
- </Button>
57
- </DialogFooter>
58
- </DialogContent>
59
- </Dialog>
60
- );
61
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/login-modal/index.tsx CHANGED
@@ -3,26 +3,25 @@ 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
- 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);
 
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);
components/pro-modal/index.tsx CHANGED
@@ -3,21 +3,20 @@ 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
- 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);
 
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);
components/ui/button.tsx CHANGED
@@ -33,7 +33,6 @@ const buttonVariants = cva(
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
  },
 
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
  },
hooks/useCallAi.ts DELETED
@@ -1,461 +0,0 @@
1
- import { useState, useRef } from "react";
2
- import { toast } from "sonner";
3
- import { MODELS } from "@/lib/providers";
4
- import { Page } from "@/types";
5
-
6
- interface UseCallAiProps {
7
- onNewPrompt: (prompt: string) => void;
8
- onSuccess: (page: Page[], p: string, n?: number[][]) => void;
9
- onScrollToBottom: () => void;
10
- setPages: React.Dispatch<React.SetStateAction<Page[]>>;
11
- setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
12
- currentPage: Page;
13
- pages: Page[];
14
- isAiWorking: boolean;
15
- setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
16
- }
17
-
18
- export const useCallAi = ({
19
- onNewPrompt,
20
- onSuccess,
21
- onScrollToBottom,
22
- setPages,
23
- setCurrentPage,
24
- pages,
25
- isAiWorking,
26
- setisAiWorking,
27
- }: UseCallAiProps) => {
28
- const audio = useRef<HTMLAudioElement | null>(null);
29
- const [controller, setController] = useState<AbortController | null>(null);
30
-
31
- const callAiNewProject = async (prompt: string, model: string | undefined, provider: string | undefined, redesignMarkdown?: string, handleThink?: (think: string) => void, onFinishThink?: () => void) => {
32
- if (isAiWorking) return;
33
- if (!redesignMarkdown && !prompt.trim()) return;
34
-
35
- setisAiWorking(true);
36
-
37
- const abortController = new AbortController();
38
- setController(abortController);
39
-
40
- try {
41
- onNewPrompt(prompt);
42
-
43
- const request = await fetch("/api/ask-ai", {
44
- method: "POST",
45
- body: JSON.stringify({
46
- prompt,
47
- provider,
48
- model,
49
- redesignMarkdown,
50
- }),
51
- headers: {
52
- "Content-Type": "application/json",
53
- "x-forwarded-for": window.location.hostname,
54
- },
55
- signal: abortController.signal,
56
- });
57
-
58
- if (request && request.body) {
59
- const reader = request.body.getReader();
60
- const decoder = new TextDecoder("utf-8");
61
- const selectedModel = MODELS.find(
62
- (m: { value: string }) => m.value === model
63
- );
64
- let contentResponse = "";
65
-
66
- const read = async () => {
67
- const { done, value } = await reader.read();
68
- if (done) {
69
- const isJson =
70
- contentResponse.trim().startsWith("{") &&
71
- contentResponse.trim().endsWith("}");
72
- const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
73
-
74
- if (jsonResponse && !jsonResponse.ok) {
75
- if (jsonResponse.openLogin) {
76
- // Handle login required
77
- return { error: "login_required" };
78
- } else if (jsonResponse.openSelectProvider) {
79
- // Handle provider selection required
80
- return { error: "provider_required", message: jsonResponse.message };
81
- } else if (jsonResponse.openProModal) {
82
- // Handle pro modal required
83
- return { error: "pro_required" };
84
- } else {
85
- toast.error(jsonResponse.message);
86
- setisAiWorking(false);
87
- return { error: "api_error", message: jsonResponse.message };
88
- }
89
- }
90
-
91
- toast.success("AI responded successfully");
92
- setisAiWorking(false);
93
-
94
- if (audio.current) audio.current.play();
95
-
96
- const newPages = formatPages(contentResponse);
97
- onSuccess(newPages, prompt);
98
-
99
- return { success: true, pages: newPages };
100
- }
101
-
102
- const chunk = decoder.decode(value, { stream: true });
103
- contentResponse += chunk;
104
-
105
- if (selectedModel?.isThinker) {
106
- const thinkMatch = contentResponse.match(/<think>[\s\S]*/)?.[0];
107
- if (thinkMatch && !contentResponse?.includes("</think>")) {
108
- handleThink?.(thinkMatch.replace("<think>", "").trim());
109
- return read();
110
- }
111
- }
112
-
113
- if (contentResponse.includes("</think>")) {
114
- onFinishThink?.();
115
- }
116
-
117
- formatPages(contentResponse);
118
- return read();
119
- };
120
-
121
- return await read();
122
- }
123
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
- } catch (error: any) {
125
- setisAiWorking(false);
126
- toast.error(error.message);
127
- if (error.openLogin) {
128
- return { error: "login_required" };
129
- }
130
- return { error: "network_error", message: error.message };
131
- }
132
- };
133
-
134
- const callAiNewPage = async (prompt: string, model: string | undefined, provider: string | undefined, currentPagePath: string, previousPrompts?: string[]) => {
135
- if (isAiWorking) return;
136
- if (!prompt.trim()) return;
137
-
138
- setisAiWorking(true);
139
-
140
- const abortController = new AbortController();
141
- setController(abortController);
142
-
143
- try {
144
- onNewPrompt(prompt);
145
-
146
- const request = await fetch("/api/ask-ai", {
147
- method: "POST",
148
- body: JSON.stringify({
149
- prompt,
150
- provider,
151
- model,
152
- pages,
153
- previousPrompts,
154
- }),
155
- headers: {
156
- "Content-Type": "application/json",
157
- "x-forwarded-for": window.location.hostname,
158
- },
159
- signal: abortController.signal,
160
- });
161
-
162
- if (request && request.body) {
163
- const reader = request.body.getReader();
164
- const decoder = new TextDecoder("utf-8");
165
- const selectedModel = MODELS.find(
166
- (m: { value: string }) => m.value === model
167
- );
168
- let contentResponse = "";
169
-
170
- const read = async () => {
171
- const { done, value } = await reader.read();
172
- if (done) {
173
- const isJson =
174
- contentResponse.trim().startsWith("{") &&
175
- contentResponse.trim().endsWith("}");
176
- const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
177
-
178
- if (jsonResponse && !jsonResponse.ok) {
179
- if (jsonResponse.openLogin) {
180
- // Handle login required
181
- return { error: "login_required" };
182
- } else if (jsonResponse.openSelectProvider) {
183
- // Handle provider selection required
184
- return { error: "provider_required", message: jsonResponse.message };
185
- } else if (jsonResponse.openProModal) {
186
- // Handle pro modal required
187
- return { error: "pro_required" };
188
- } else {
189
- toast.error(jsonResponse.message);
190
- setisAiWorking(false);
191
- return { error: "api_error", message: jsonResponse.message };
192
- }
193
- }
194
-
195
- toast.success("AI responded successfully");
196
- setisAiWorking(false);
197
-
198
- if (selectedModel?.isThinker) {
199
- // Reset to default model if using thinker model
200
- // Note: You might want to add a callback for this
201
- }
202
-
203
- if (audio.current) audio.current.play();
204
-
205
- const newPage = formatPage(contentResponse, currentPagePath);
206
- if (!newPage) { return { error: "api_error", message: "Failed to format page" } }
207
- onSuccess([...pages, newPage], prompt);
208
-
209
- return { success: true, pages: [...pages, newPage] };
210
- }
211
-
212
- const chunk = decoder.decode(value, { stream: true });
213
- contentResponse += chunk;
214
-
215
- if (selectedModel?.isThinker) {
216
- const thinkMatch = contentResponse.match(/<think>[\s\S]*/)?.[0];
217
- if (thinkMatch && !contentResponse?.includes("</think>")) {
218
- // contentThink += chunk;
219
- return read();
220
- }
221
- }
222
-
223
- formatPage(contentResponse, currentPagePath);
224
- return read();
225
- };
226
-
227
- return await read();
228
- }
229
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
- } catch (error: any) {
231
- setisAiWorking(false);
232
- toast.error(error.message);
233
- if (error.openLogin) {
234
- return { error: "login_required" };
235
- }
236
- return { error: "network_error", message: error.message };
237
- }
238
- };
239
-
240
- const callAiFollowUp = async (prompt: string, model: string | undefined, provider: string | undefined, previousPrompts: string[], selectedElementHtml?: string, files?: string[]) => {
241
- if (isAiWorking) return;
242
- if (!prompt.trim()) return;
243
-
244
- setisAiWorking(true);
245
-
246
- const abortController = new AbortController();
247
- setController(abortController);
248
-
249
- try {
250
- onNewPrompt(prompt);
251
-
252
- const request = await fetch("/api/ask-ai", {
253
- method: "PUT",
254
- body: JSON.stringify({
255
- prompt,
256
- provider,
257
- previousPrompts,
258
- model,
259
- pages,
260
- selectedElementHtml,
261
- files,
262
- }),
263
- headers: {
264
- "Content-Type": "application/json",
265
- "x-forwarded-for": window.location.hostname,
266
- },
267
- signal: abortController.signal,
268
- });
269
-
270
- if (request && request.body) {
271
- const res = await request.json();
272
-
273
- if (!request.ok) {
274
- if (res.openLogin) {
275
- setisAiWorking(false);
276
- return { error: "login_required" };
277
- } else if (res.openSelectProvider) {
278
- setisAiWorking(false);
279
- return { error: "provider_required", message: res.message };
280
- } else if (res.openProModal) {
281
- setisAiWorking(false);
282
- return { error: "pro_required" };
283
- } else {
284
- toast.error(res.message);
285
- setisAiWorking(false);
286
- return { error: "api_error", message: res.message };
287
- }
288
- }
289
-
290
- toast.success("AI responded successfully");
291
- setisAiWorking(false);
292
-
293
- setPages(res.pages);
294
- onSuccess(res.pages, prompt, res.updatedLines);
295
-
296
- if (audio.current) audio.current.play();
297
-
298
- return { success: true, html: res.html, updatedLines: res.updatedLines };
299
- }
300
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
301
- } catch (error: any) {
302
- setisAiWorking(false);
303
- toast.error(error.message);
304
- if (error.openLogin) {
305
- return { error: "login_required" };
306
- }
307
- return { error: "network_error", message: error.message };
308
- }
309
- };
310
-
311
- // Stop the current AI generation
312
- const stopController = () => {
313
- if (controller) {
314
- controller.abort();
315
- setController(null);
316
- setisAiWorking(false);
317
- }
318
- };
319
-
320
- const formatPages = (content: string) => {
321
- const pages: Page[] = [];
322
- if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) {
323
- return pages;
324
- }
325
-
326
- const cleanedContent = content.replace(
327
- /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/,
328
- "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE"
329
- );
330
- const htmlChunks = cleanedContent.split(
331
- /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/
332
- );
333
- const processedChunks = new Set<number>();
334
-
335
- htmlChunks.forEach((chunk, index) => {
336
- if (processedChunks.has(index) || !chunk?.trim()) {
337
- return;
338
- }
339
- const htmlContent = extractHtmlContent(htmlChunks[index + 1]);
340
-
341
- if (htmlContent) {
342
- const page: Page = {
343
- path: chunk.trim(),
344
- html: htmlContent,
345
- };
346
- pages.push(page);
347
-
348
- if (htmlContent.length > 200) {
349
- onScrollToBottom();
350
- }
351
-
352
- processedChunks.add(index);
353
- processedChunks.add(index + 1);
354
- }
355
- });
356
- if (pages.length > 0) {
357
- setPages(pages);
358
- const lastPagePath = pages[pages.length - 1]?.path;
359
- setCurrentPage(lastPagePath || "index.html");
360
- }
361
-
362
- return pages;
363
- };
364
-
365
- const formatPage = (content: string, currentPagePath: string) => {
366
- if (!content.match(/<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/)) {
367
- return null;
368
- }
369
-
370
- const cleanedContent = content.replace(
371
- /[\s\S]*?<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/,
372
- "<<<<<<< START_TITLE $1 >>>>>>> END_TITLE"
373
- );
374
-
375
- const htmlChunks = cleanedContent.split(
376
- /<<<<<<< START_TITLE (.*?) >>>>>>> END_TITLE/
377
- )?.filter(Boolean);
378
-
379
- const pagePath = htmlChunks[0]?.trim() || "";
380
- const htmlContent = extractHtmlContent(htmlChunks[1]);
381
-
382
- if (!pagePath || !htmlContent) {
383
- return null;
384
- }
385
-
386
- const page: Page = {
387
- path: pagePath,
388
- html: htmlContent,
389
- };
390
-
391
- setPages(prevPages => {
392
- const existingPageIndex = prevPages.findIndex(p => p.path === currentPagePath || p.path === pagePath);
393
-
394
- if (existingPageIndex !== -1) {
395
- const updatedPages = [...prevPages];
396
- updatedPages[existingPageIndex] = page;
397
- return updatedPages;
398
- } else {
399
- return [...prevPages, page];
400
- }
401
- });
402
-
403
- setCurrentPage(pagePath);
404
-
405
- if (htmlContent.length > 200) {
406
- onScrollToBottom();
407
- }
408
-
409
- return page;
410
- };
411
-
412
- // Helper function to extract and clean HTML content
413
- const extractHtmlContent = (chunk: string): string => {
414
- if (!chunk) return "";
415
-
416
- // Extract HTML content
417
- const htmlMatch = chunk.trim().match(/<!DOCTYPE html>[\s\S]*/);
418
- if (!htmlMatch) return "";
419
-
420
- let htmlContent = htmlMatch[0];
421
-
422
- // Ensure proper HTML structure
423
- htmlContent = ensureCompleteHtml(htmlContent);
424
-
425
- // Remove markdown code blocks if present
426
- htmlContent = htmlContent.replace(/```/g, "");
427
-
428
- return htmlContent;
429
- };
430
-
431
- // Helper function to ensure HTML has complete structure
432
- const ensureCompleteHtml = (html: string): string => {
433
- let completeHtml = html;
434
-
435
- // Add missing head closing tag
436
- if (completeHtml.includes("<head>") && !completeHtml.includes("</head>")) {
437
- completeHtml += "\n</head>";
438
- }
439
-
440
- // Add missing body closing tag
441
- if (completeHtml.includes("<body") && !completeHtml.includes("</body>")) {
442
- completeHtml += "\n</body>";
443
- }
444
-
445
- // Add missing html closing tag
446
- if (!completeHtml.includes("</html>")) {
447
- completeHtml += "\n</html>";
448
- }
449
-
450
- return completeHtml;
451
- };
452
-
453
- return {
454
- callAiNewProject,
455
- callAiFollowUp,
456
- callAiNewPage,
457
- stopController,
458
- controller,
459
- audio,
460
- };
461
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hooks/useEditor.ts CHANGED
@@ -1,18 +1,12 @@
1
- import { defaultHTML } from "@/lib/consts";
2
- import { HtmlHistory, Page } from "@/types";
3
  import { useState } from "react";
4
 
5
- export const useEditor = (initialPages?: Page[], initialPrompts?: string[], initialHtmlStorage?: string) => {
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: initialHtmlStorage ?? 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)
@@ -23,17 +17,14 @@ export const useEditor = (initialPages?: Page[], initialPrompts?: string[], init
23
  * State to manage the prompts used for generating HTML content.
24
  * This can be used to track what prompts were used in the editor.
25
  */
26
- const [prompts, setPrompts] = useState<string[]>(
27
- initialPrompts ?? []
28
- );
29
-
30
 
31
  return {
 
 
32
  htmlHistory,
33
  setHtmlHistory,
34
  prompts,
35
- pages,
36
- setPages,
37
  setPrompts,
38
  };
39
  };
 
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)
 
17
  * State to manage the prompts used for generating HTML content.
18
  * This can be used to track what prompts were used in the editor.
19
  */
20
+ const [prompts, setPrompts] = useState<string[]>([]);
 
 
 
21
 
22
  return {
23
+ html,
24
+ setHtml,
25
  htmlHistory,
26
  setHtmlHistory,
27
  prompts,
 
 
28
  setPrompts,
29
  };
30
  };
lib/prompts.ts CHANGED
@@ -2,81 +2,24 @@ 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 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
- // TODO REVIEW LINK. MAYBE GO BACK TO SANDPACK.
13
- // FIX PREVIEW LINK NOT WORKING ONCE THE SITE IS DEPLOYED.
14
-
15
- export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer.
16
- You create website in a way a designer would, using ONLY HTML, CSS and Javascript.
17
- Try to create the best UI possible. Important: Make the website responsive by using TailwindCSS. Use it as much as you can, if you can't use it, use custom css (make sure to import tailwind with <script src="https://cdn.tailwindcss.com"></script> in the head).
18
- Also try to elaborate as much as you can, to create something unique, with a great design.
19
- If you want to use ICONS import Feather Icons (Make sure to add <script src="https://unpkg.com/feather-icons"></script> and <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> in the head., and <script>feather.replace();</script> in the body. Ex : <i data-feather="user"></i>).
20
- If you want to use animations you can use: Animejs.com (Make sure to add <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script> and <script>const { animate } = anime;</script> in the head.), AOS.com (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>).
21
- You can create multiple pages website at once (following the format rules below) or a Single Page Application. If the user doesn't ask for a specific version, you have to determine the best version for the user, depending on the request. (Try to avoid the Single Page Application if the user asks for multiple pages.)
22
- No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user.
23
- Return the results in a \`\`\`html\`\`\` markdown. Format the results like:
24
- 1. Start with ${TITLE_PAGE_START}.
25
- 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.
26
- 3. Close the start tag with the ${TITLE_PAGE_END}.
27
- 4. Start the HTML response with the triple backticks, like \`\`\`html.
28
- 5. Insert the following html there.
29
- 6. Close with the triple backticks, like \`\`\`.
30
- 7. Retry if another pages.
31
- Example Code:
32
- ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
33
- \`\`\`html
34
- <!DOCTYPE html>
35
- <html lang="en">
36
- <head>
37
- <meta charset="UTF-8">
38
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
39
- <title>Index</title>
40
- <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
41
- <script src="https://cdn.tailwindcss.com"></script>
42
- <link href="https://unpkg.com/[email protected]/dist/aos.css" rel="stylesheet">
43
- <script src="https://unpkg.com/[email protected]/dist/aos.js"></script>
44
- <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
45
- <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script>
46
- <script src="https://unpkg.com/feather-icons"></script>
47
- </head>
48
- <body>
49
- <h1>Hello World</h1>
50
- <script>AOS.init();</script>
51
- <script>const { animate } = anime;</script>
52
- <script>feather.replace();</script>
53
- </body>
54
- </html>
55
- \`\`\`
56
- IMPORTANT: The first file should be always named index.html.`
57
-
58
- export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying an existing HTML files.
59
- The user wants to apply changes and probably add new features/pages to the website, based on their request.
60
- You MUST output ONLY the changes required using the following UPDATE_PAGE_START and SEARCH/REPLACE format. Do NOT output the entire file.
61
- If it's a new page, you MUST applied the following NEW_PAGE_START and UPDATE_PAGE_END format.
62
- Do NOT explain the changes or what you did, just return the expected results.
63
- Update Format Rules:
64
- 1. Start with ${UPDATE_PAGE_START}
65
- 2. Provide the name of the page you are modifying.
66
- 3. Close the start tag with the ${UPDATE_PAGE_END}.
67
- 4. Start with ${SEARCH_START}
68
- 5. Provide the exact lines from the current code that need to be replaced.
69
- 6. Use ${DIVIDER} to separate the search block from the replacement.
70
- 7. Provide the new lines that should replace the original lines.
71
- 8. End with ${REPLACE_END}
72
- 9. You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the file.
73
- 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.
74
- 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).
75
- 12. IMPORTANT: The SEARCH block must *exactly* match the current code, including indentation and whitespace.
76
  Example Modifying Code:
77
  \`\`\`
78
  Some explanation...
79
- ${UPDATE_PAGE_START}index.html${UPDATE_PAGE_END}
80
  ${SEARCH_START}
81
  <h1>Old Title</h1>
82
  ${DIVIDER}
@@ -92,44 +35,8 @@ ${REPLACE_END}
92
  Example Deleting Code:
93
  \`\`\`
94
  Removing the paragraph...
95
- ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
96
  ${SEARCH_START}
97
  <p>This paragraph will be deleted.</p>
98
  ${DIVIDER}
99
  ${REPLACE_END}
100
- \`\`\`
101
- The user can also ask to add a new page, in this case you should return the new page in the following format:
102
- 1. Start with ${NEW_PAGE_START}.
103
- 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.
104
- 3. Close the start tag with the ${NEW_PAGE_END}.
105
- 4. Start the HTML response with the triple backticks, like \`\`\`html.
106
- 5. Insert the following html there.
107
- 6. Close with the triple backticks, like \`\`\`.
108
- 7. Retry if another pages.
109
- Example Code:
110
- ${NEW_PAGE_START}index.html${NEW_PAGE_END}
111
- \`\`\`html
112
- <!DOCTYPE html>
113
- <html lang="en">
114
- <head>
115
- <meta charset="UTF-8">
116
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
117
- <title>Index</title>
118
- <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
119
- <script src="https://cdn.tailwindcss.com"></script>
120
- <link href="https://unpkg.com/[email protected]/dist/aos.css" rel="stylesheet">
121
- <script src="https://unpkg.com/[email protected]/dist/aos.js"></script>
122
- <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
123
- <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script>
124
- <script src="https://unpkg.com/feather-icons"></script>
125
- </head>
126
- <body>
127
- <h1>Hello World</h1>
128
- <script>AOS.init();</script>
129
- <script>const { animate } = anime;</script>
130
- <script>feather.replace();</script>
131
- </body>
132
- </html>
133
- \`\`\`
134
- IMPORTANT: While creating a new page, UPDATE ALL THE OTHERS (using the UPDATE_PAGE_START and SEARCH/REPLACE format) pages to add or replace the link to the new page, otherwise the user will not be able to navigate to the new page. (Dont use onclick to navigate, only href)
135
- No need to explain what you did. Just return the expected result.`
 
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
  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
+ \`\`\``;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/providers.ts CHANGED
@@ -70,11 +70,4 @@ export const MODELS = [
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
- autoProvider: "fireworks-ai",
79
- },
80
  ];
 
70
  providers: ["together", "novita", "groq"],
71
  autoProvider: "groq",
72
  },
 
 
 
 
 
 
 
73
  ];
next.config.ts CHANGED
@@ -24,9 +24,6 @@ const nextConfig: NextConfig = {
24
 
25
  return config;
26
  },
27
- images: {
28
- remotePatterns: [new URL('https://huggingface.co/**')],
29
- },
30
  };
31
 
32
  export default nextConfig;
 
24
 
25
  return config;
26
  },
 
 
 
27
  };
28
 
29
  export default nextConfig;
package-lock.json CHANGED
@@ -8,7 +8,6 @@
8
  "name": "deepsite-v2",
9
  "version": "0.1.0",
10
  "dependencies": {
11
- "@codesandbox/sandpack-react": "^2.20.0",
12
  "@huggingface/hub": "^2.2.0",
13
  "@huggingface/inference": "^4.0.3",
14
  "@monaco-editor/react": "^4.7.0-rc.0",
@@ -98,182 +97,6 @@
98
  "node": ">=6.9.0"
99
  }
100
  },
101
- "node_modules/@codemirror/autocomplete": {
102
- "version": "6.18.6",
103
- "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
104
- "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
105
- "license": "MIT",
106
- "dependencies": {
107
- "@codemirror/language": "^6.0.0",
108
- "@codemirror/state": "^6.0.0",
109
- "@codemirror/view": "^6.17.0",
110
- "@lezer/common": "^1.0.0"
111
- }
112
- },
113
- "node_modules/@codemirror/commands": {
114
- "version": "6.8.1",
115
- "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
116
- "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
117
- "license": "MIT",
118
- "dependencies": {
119
- "@codemirror/language": "^6.0.0",
120
- "@codemirror/state": "^6.4.0",
121
- "@codemirror/view": "^6.27.0",
122
- "@lezer/common": "^1.1.0"
123
- }
124
- },
125
- "node_modules/@codemirror/lang-css": {
126
- "version": "6.3.1",
127
- "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
128
- "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
129
- "license": "MIT",
130
- "dependencies": {
131
- "@codemirror/autocomplete": "^6.0.0",
132
- "@codemirror/language": "^6.0.0",
133
- "@codemirror/state": "^6.0.0",
134
- "@lezer/common": "^1.0.2",
135
- "@lezer/css": "^1.1.7"
136
- }
137
- },
138
- "node_modules/@codemirror/lang-html": {
139
- "version": "6.4.9",
140
- "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
141
- "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
142
- "license": "MIT",
143
- "dependencies": {
144
- "@codemirror/autocomplete": "^6.0.0",
145
- "@codemirror/lang-css": "^6.0.0",
146
- "@codemirror/lang-javascript": "^6.0.0",
147
- "@codemirror/language": "^6.4.0",
148
- "@codemirror/state": "^6.0.0",
149
- "@codemirror/view": "^6.17.0",
150
- "@lezer/common": "^1.0.0",
151
- "@lezer/css": "^1.1.0",
152
- "@lezer/html": "^1.3.0"
153
- }
154
- },
155
- "node_modules/@codemirror/lang-javascript": {
156
- "version": "6.2.4",
157
- "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
158
- "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
159
- "license": "MIT",
160
- "dependencies": {
161
- "@codemirror/autocomplete": "^6.0.0",
162
- "@codemirror/language": "^6.6.0",
163
- "@codemirror/lint": "^6.0.0",
164
- "@codemirror/state": "^6.0.0",
165
- "@codemirror/view": "^6.17.0",
166
- "@lezer/common": "^1.0.0",
167
- "@lezer/javascript": "^1.0.0"
168
- }
169
- },
170
- "node_modules/@codemirror/language": {
171
- "version": "6.11.3",
172
- "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
173
- "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
174
- "license": "MIT",
175
- "dependencies": {
176
- "@codemirror/state": "^6.0.0",
177
- "@codemirror/view": "^6.23.0",
178
- "@lezer/common": "^1.1.0",
179
- "@lezer/highlight": "^1.0.0",
180
- "@lezer/lr": "^1.0.0",
181
- "style-mod": "^4.0.0"
182
- }
183
- },
184
- "node_modules/@codemirror/lint": {
185
- "version": "6.8.5",
186
- "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
187
- "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
188
- "license": "MIT",
189
- "dependencies": {
190
- "@codemirror/state": "^6.0.0",
191
- "@codemirror/view": "^6.35.0",
192
- "crelt": "^1.0.5"
193
- }
194
- },
195
- "node_modules/@codemirror/state": {
196
- "version": "6.5.2",
197
- "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
198
- "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
199
- "license": "MIT",
200
- "dependencies": {
201
- "@marijn/find-cluster-break": "^1.0.0"
202
- }
203
- },
204
- "node_modules/@codemirror/view": {
205
- "version": "6.38.1",
206
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
207
- "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
208
- "license": "MIT",
209
- "dependencies": {
210
- "@codemirror/state": "^6.5.0",
211
- "crelt": "^1.0.6",
212
- "style-mod": "^4.1.0",
213
- "w3c-keyname": "^2.2.4"
214
- }
215
- },
216
- "node_modules/@codesandbox/nodebox": {
217
- "version": "0.1.8",
218
- "resolved": "https://registry.npmjs.org/@codesandbox/nodebox/-/nodebox-0.1.8.tgz",
219
- "integrity": "sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg==",
220
- "license": "SEE LICENSE IN ./LICENSE",
221
- "dependencies": {
222
- "outvariant": "^1.4.0",
223
- "strict-event-emitter": "^0.4.3"
224
- }
225
- },
226
- "node_modules/@codesandbox/sandpack-client": {
227
- "version": "2.19.8",
228
- "resolved": "https://registry.npmjs.org/@codesandbox/sandpack-client/-/sandpack-client-2.19.8.tgz",
229
- "integrity": "sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ==",
230
- "license": "Apache-2.0",
231
- "dependencies": {
232
- "@codesandbox/nodebox": "0.1.8",
233
- "buffer": "^6.0.3",
234
- "dequal": "^2.0.2",
235
- "mime-db": "^1.52.0",
236
- "outvariant": "1.4.0",
237
- "static-browser-server": "1.0.3"
238
- }
239
- },
240
- "node_modules/@codesandbox/sandpack-react": {
241
- "version": "2.20.0",
242
- "resolved": "https://registry.npmjs.org/@codesandbox/sandpack-react/-/sandpack-react-2.20.0.tgz",
243
- "integrity": "sha512-takd1YpW/PMQ6KPQfvseWLHWklJovGY8QYj8MtWnskGKbjOGJ6uZfyZbcJ6aCFLQMpNyjTqz9AKNbvhCOZ1TUQ==",
244
- "license": "Apache-2.0",
245
- "dependencies": {
246
- "@codemirror/autocomplete": "^6.4.0",
247
- "@codemirror/commands": "^6.1.3",
248
- "@codemirror/lang-css": "^6.0.1",
249
- "@codemirror/lang-html": "^6.4.0",
250
- "@codemirror/lang-javascript": "^6.1.2",
251
- "@codemirror/language": "^6.3.2",
252
- "@codemirror/state": "^6.2.0",
253
- "@codemirror/view": "^6.7.1",
254
- "@codesandbox/sandpack-client": "^2.19.8",
255
- "@lezer/highlight": "^1.1.3",
256
- "@react-hook/intersection-observer": "^3.1.1",
257
- "@stitches/core": "^1.2.6",
258
- "anser": "^2.1.1",
259
- "clean-set": "^1.1.2",
260
- "dequal": "^2.0.2",
261
- "escape-carriage": "^1.3.1",
262
- "lz-string": "^1.4.4",
263
- "react-devtools-inline": "4.4.0",
264
- "react-is": "^17.0.2"
265
- },
266
- "peerDependencies": {
267
- "react": "^16.8.0 || ^17 || ^18 || ^19",
268
- "react-dom": "^16.8.0 || ^17 || ^18 || ^19"
269
- }
270
- },
271
- "node_modules/@codesandbox/sandpack-react/node_modules/react-is": {
272
- "version": "17.0.2",
273
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
274
- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
275
- "license": "MIT"
276
- },
277
  "node_modules/@emnapi/core": {
278
  "version": "1.4.3",
279
  "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
@@ -1053,69 +876,6 @@
1053
  "@jridgewell/sourcemap-codec": "^1.4.14"
1054
  }
1055
  },
1056
- "node_modules/@lezer/common": {
1057
- "version": "1.2.3",
1058
- "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
1059
- "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
1060
- "license": "MIT"
1061
- },
1062
- "node_modules/@lezer/css": {
1063
- "version": "1.3.0",
1064
- "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
1065
- "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
1066
- "license": "MIT",
1067
- "dependencies": {
1068
- "@lezer/common": "^1.2.0",
1069
- "@lezer/highlight": "^1.0.0",
1070
- "@lezer/lr": "^1.3.0"
1071
- }
1072
- },
1073
- "node_modules/@lezer/highlight": {
1074
- "version": "1.2.1",
1075
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
1076
- "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
1077
- "license": "MIT",
1078
- "dependencies": {
1079
- "@lezer/common": "^1.0.0"
1080
- }
1081
- },
1082
- "node_modules/@lezer/html": {
1083
- "version": "1.3.10",
1084
- "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
1085
- "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
1086
- "license": "MIT",
1087
- "dependencies": {
1088
- "@lezer/common": "^1.2.0",
1089
- "@lezer/highlight": "^1.0.0",
1090
- "@lezer/lr": "^1.0.0"
1091
- }
1092
- },
1093
- "node_modules/@lezer/javascript": {
1094
- "version": "1.5.1",
1095
- "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
1096
- "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
1097
- "license": "MIT",
1098
- "dependencies": {
1099
- "@lezer/common": "^1.2.0",
1100
- "@lezer/highlight": "^1.1.3",
1101
- "@lezer/lr": "^1.3.0"
1102
- }
1103
- },
1104
- "node_modules/@lezer/lr": {
1105
- "version": "1.4.2",
1106
- "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
1107
- "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
1108
- "license": "MIT",
1109
- "dependencies": {
1110
- "@lezer/common": "^1.0.0"
1111
- }
1112
- },
1113
- "node_modules/@marijn/find-cluster-break": {
1114
- "version": "1.0.2",
1115
- "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
1116
- "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
1117
- "license": "MIT"
1118
- },
1119
  "node_modules/@monaco-editor/loader": {
1120
  "version": "1.5.0",
1121
  "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
@@ -1350,12 +1110,6 @@
1350
  "node": ">=12.4.0"
1351
  }
1352
  },
1353
- "node_modules/@open-draft/deferred-promise": {
1354
- "version": "2.2.0",
1355
- "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
1356
- "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
1357
- "license": "MIT"
1358
- },
1359
  "node_modules/@radix-ui/number": {
1360
  "version": "1.1.1",
1361
  "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -2301,28 +2055,6 @@
2301
  "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
2302
  "license": "MIT"
2303
  },
2304
- "node_modules/@react-hook/intersection-observer": {
2305
- "version": "3.1.2",
2306
- "resolved": "https://registry.npmjs.org/@react-hook/intersection-observer/-/intersection-observer-3.1.2.tgz",
2307
- "integrity": "sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==",
2308
- "license": "MIT",
2309
- "dependencies": {
2310
- "@react-hook/passive-layout-effect": "^1.2.0",
2311
- "intersection-observer": "^0.10.0"
2312
- },
2313
- "peerDependencies": {
2314
- "react": ">=16.8"
2315
- }
2316
- },
2317
- "node_modules/@react-hook/passive-layout-effect": {
2318
- "version": "1.2.1",
2319
- "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz",
2320
- "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==",
2321
- "license": "MIT",
2322
- "peerDependencies": {
2323
- "react": ">=16.8"
2324
- }
2325
- },
2326
  "node_modules/@rtsao/scc": {
2327
  "version": "1.1.0",
2328
  "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2337,12 +2069,6 @@
2337
  "dev": true,
2338
  "license": "MIT"
2339
  },
2340
- "node_modules/@stitches/core": {
2341
- "version": "1.2.8",
2342
- "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz",
2343
- "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==",
2344
- "license": "MIT"
2345
- },
2346
  "node_modules/@swc/counter": {
2347
  "version": "0.1.3",
2348
  "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -3615,12 +3341,6 @@
3615
  "ajv": "^6.9.1"
3616
  }
3617
  },
3618
- "node_modules/anser": {
3619
- "version": "2.3.2",
3620
- "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.2.tgz",
3621
- "integrity": "sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw==",
3622
- "license": "MIT"
3623
- },
3624
  "node_modules/ansi-styles": {
3625
  "version": "4.3.0",
3626
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -3900,26 +3620,6 @@
3900
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
3901
  "license": "MIT"
3902
  },
3903
- "node_modules/base64-js": {
3904
- "version": "1.5.1",
3905
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
3906
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
3907
- "funding": [
3908
- {
3909
- "type": "github",
3910
- "url": "https://github.com/sponsors/feross"
3911
- },
3912
- {
3913
- "type": "patreon",
3914
- "url": "https://www.patreon.com/feross"
3915
- },
3916
- {
3917
- "type": "consulting",
3918
- "url": "https://feross.org/support"
3919
- }
3920
- ],
3921
- "license": "MIT"
3922
- },
3923
  "node_modules/big.js": {
3924
  "version": "5.2.2",
3925
  "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -3995,30 +3695,6 @@
3995
  "node": ">=16.20.1"
3996
  }
3997
  },
3998
- "node_modules/buffer": {
3999
- "version": "6.0.3",
4000
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
4001
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
4002
- "funding": [
4003
- {
4004
- "type": "github",
4005
- "url": "https://github.com/sponsors/feross"
4006
- },
4007
- {
4008
- "type": "patreon",
4009
- "url": "https://www.patreon.com/feross"
4010
- },
4011
- {
4012
- "type": "consulting",
4013
- "url": "https://feross.org/support"
4014
- }
4015
- ],
4016
- "license": "MIT",
4017
- "dependencies": {
4018
- "base64-js": "^1.3.1",
4019
- "ieee754": "^1.2.1"
4020
- }
4021
- },
4022
  "node_modules/buffer-from": {
4023
  "version": "1.1.2",
4024
  "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -4171,12 +3847,6 @@
4171
  "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
4172
  "license": "MIT"
4173
  },
4174
- "node_modules/clean-set": {
4175
- "version": "1.1.2",
4176
- "resolved": "https://registry.npmjs.org/clean-set/-/clean-set-1.1.2.tgz",
4177
- "integrity": "sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug==",
4178
- "license": "MIT"
4179
- },
4180
  "node_modules/client-only": {
4181
  "version": "0.0.1",
4182
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -4270,12 +3940,6 @@
4270
  "toggle-selection": "^1.0.6"
4271
  }
4272
  },
4273
- "node_modules/crelt": {
4274
- "version": "1.0.6",
4275
- "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
4276
- "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
4277
- "license": "MIT"
4278
- },
4279
  "node_modules/cross-spawn": {
4280
  "version": "7.0.6",
4281
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4318,19 +3982,6 @@
4318
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
4319
  "license": "MIT"
4320
  },
4321
- "node_modules/d": {
4322
- "version": "1.0.2",
4323
- "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
4324
- "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
4325
- "license": "ISC",
4326
- "dependencies": {
4327
- "es5-ext": "^0.10.64",
4328
- "type": "^2.7.2"
4329
- },
4330
- "engines": {
4331
- "node": ">=0.12"
4332
- }
4333
- },
4334
  "node_modules/damerau-levenshtein": {
4335
  "version": "1.0.8",
4336
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4470,15 +4121,6 @@
4470
  "node": ">=0.4.0"
4471
  }
4472
  },
4473
- "node_modules/dequal": {
4474
- "version": "2.0.3",
4475
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
4476
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
4477
- "license": "MIT",
4478
- "engines": {
4479
- "node": ">=6"
4480
- }
4481
- },
4482
  "node_modules/detect-libc": {
4483
  "version": "2.0.4",
4484
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -4508,18 +4150,6 @@
4508
  "node": ">=0.10.0"
4509
  }
4510
  },
4511
- "node_modules/dotenv": {
4512
- "version": "16.6.1",
4513
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
4514
- "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
4515
- "license": "BSD-2-Clause",
4516
- "engines": {
4517
- "node": ">=12"
4518
- },
4519
- "funding": {
4520
- "url": "https://dotenvx.com"
4521
- }
4522
- },
4523
  "node_modules/dunder-proto": {
4524
  "version": "1.0.1",
4525
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4763,46 +4393,6 @@
4763
  "url": "https://github.com/sponsors/ljharb"
4764
  }
4765
  },
4766
- "node_modules/es5-ext": {
4767
- "version": "0.10.64",
4768
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
4769
- "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
4770
- "hasInstallScript": true,
4771
- "license": "ISC",
4772
- "dependencies": {
4773
- "es6-iterator": "^2.0.3",
4774
- "es6-symbol": "^3.1.3",
4775
- "esniff": "^2.0.1",
4776
- "next-tick": "^1.1.0"
4777
- },
4778
- "engines": {
4779
- "node": ">=0.10"
4780
- }
4781
- },
4782
- "node_modules/es6-iterator": {
4783
- "version": "2.0.3",
4784
- "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
4785
- "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
4786
- "license": "MIT",
4787
- "dependencies": {
4788
- "d": "1",
4789
- "es5-ext": "^0.10.35",
4790
- "es6-symbol": "^3.1.1"
4791
- }
4792
- },
4793
- "node_modules/es6-symbol": {
4794
- "version": "3.1.4",
4795
- "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
4796
- "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
4797
- "license": "ISC",
4798
- "dependencies": {
4799
- "d": "^1.0.2",
4800
- "ext": "^1.7.0"
4801
- },
4802
- "engines": {
4803
- "node": ">=0.12"
4804
- }
4805
- },
4806
  "node_modules/escalade": {
4807
  "version": "3.2.0",
4808
  "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4814,12 +4404,6 @@
4814
  "node": ">=6"
4815
  }
4816
  },
4817
- "node_modules/escape-carriage": {
4818
- "version": "1.3.1",
4819
- "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
4820
- "integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==",
4821
- "license": "MIT"
4822
- },
4823
  "node_modules/escape-string-regexp": {
4824
  "version": "4.0.0",
4825
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -5191,21 +4775,6 @@
5191
  "url": "https://opencollective.com/eslint"
5192
  }
5193
  },
5194
- "node_modules/esniff": {
5195
- "version": "2.0.1",
5196
- "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
5197
- "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
5198
- "license": "ISC",
5199
- "dependencies": {
5200
- "d": "^1.0.1",
5201
- "es5-ext": "^0.10.62",
5202
- "event-emitter": "^0.3.5",
5203
- "type": "^2.7.2"
5204
- },
5205
- "engines": {
5206
- "node": ">=0.10"
5207
- }
5208
- },
5209
  "node_modules/espree": {
5210
  "version": "10.3.0",
5211
  "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
@@ -5265,16 +4834,6 @@
5265
  "node": ">=0.10.0"
5266
  }
5267
  },
5268
- "node_modules/event-emitter": {
5269
- "version": "0.3.5",
5270
- "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
5271
- "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
5272
- "license": "MIT",
5273
- "dependencies": {
5274
- "d": "1",
5275
- "es5-ext": "~0.10.14"
5276
- }
5277
- },
5278
  "node_modules/events": {
5279
  "version": "3.3.0",
5280
  "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -5286,15 +4845,6 @@
5286
  "node": ">=0.8.x"
5287
  }
5288
  },
5289
- "node_modules/ext": {
5290
- "version": "1.7.0",
5291
- "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
5292
- "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
5293
- "license": "ISC",
5294
- "dependencies": {
5295
- "type": "^2.7.2"
5296
- }
5297
- },
5298
  "node_modules/fast-deep-equal": {
5299
  "version": "3.1.3",
5300
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5801,26 +5351,6 @@
5801
  "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
5802
  "license": "BSD-3-Clause"
5803
  },
5804
- "node_modules/ieee754": {
5805
- "version": "1.2.1",
5806
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
5807
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
5808
- "funding": [
5809
- {
5810
- "type": "github",
5811
- "url": "https://github.com/sponsors/feross"
5812
- },
5813
- {
5814
- "type": "patreon",
5815
- "url": "https://www.patreon.com/feross"
5816
- },
5817
- {
5818
- "type": "consulting",
5819
- "url": "https://feross.org/support"
5820
- }
5821
- ],
5822
- "license": "BSD-3-Clause"
5823
- },
5824
  "node_modules/ignore": {
5825
  "version": "5.3.2",
5826
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5879,12 +5409,6 @@
5879
  "node": ">= 0.4"
5880
  }
5881
  },
5882
- "node_modules/intersection-observer": {
5883
- "version": "0.10.0",
5884
- "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz",
5885
- "integrity": "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==",
5886
- "license": "W3C-20150513"
5887
- },
5888
  "node_modules/is-array-buffer": {
5889
  "version": "3.0.5",
5890
  "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -6816,15 +6340,6 @@
6816
  "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
6817
  }
6818
  },
6819
- "node_modules/lz-string": {
6820
- "version": "1.5.0",
6821
- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
6822
- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
6823
- "license": "MIT",
6824
- "bin": {
6825
- "lz-string": "bin/bin.js"
6826
- }
6827
- },
6828
  "node_modules/magic-string": {
6829
  "version": "0.30.17",
6830
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@@ -7211,12 +6726,6 @@
7211
  "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
7212
  }
7213
  },
7214
- "node_modules/next-tick": {
7215
- "version": "1.1.0",
7216
- "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
7217
- "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
7218
- "license": "ISC"
7219
- },
7220
  "node_modules/next/node_modules/postcss": {
7221
  "version": "8.4.31",
7222
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -7393,12 +6902,6 @@
7393
  "node": ">= 0.8.0"
7394
  }
7395
  },
7396
- "node_modules/outvariant": {
7397
- "version": "1.4.0",
7398
- "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz",
7399
- "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==",
7400
- "license": "MIT"
7401
- },
7402
  "node_modules/own-keys": {
7403
  "version": "1.0.1",
7404
  "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -7617,15 +7120,6 @@
7617
  "node": ">=0.10.0"
7618
  }
7619
  },
7620
- "node_modules/react-devtools-inline": {
7621
- "version": "4.4.0",
7622
- "resolved": "https://registry.npmjs.org/react-devtools-inline/-/react-devtools-inline-4.4.0.tgz",
7623
- "integrity": "sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ==",
7624
- "license": "MIT",
7625
- "dependencies": {
7626
- "es6-symbol": "^3"
7627
- }
7628
- },
7629
  "node_modules/react-dom": {
7630
  "version": "19.1.0",
7631
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@@ -8364,18 +7858,6 @@
8364
  "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
8365
  "license": "MIT"
8366
  },
8367
- "node_modules/static-browser-server": {
8368
- "version": "1.0.3",
8369
- "resolved": "https://registry.npmjs.org/static-browser-server/-/static-browser-server-1.0.3.tgz",
8370
- "integrity": "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==",
8371
- "license": "Apache-2.0",
8372
- "dependencies": {
8373
- "@open-draft/deferred-promise": "^2.1.0",
8374
- "dotenv": "^16.0.3",
8375
- "mime-db": "^1.52.0",
8376
- "outvariant": "^1.3.0"
8377
- }
8378
- },
8379
  "node_modules/stop-iteration-iterator": {
8380
  "version": "1.1.0",
8381
  "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -8398,12 +7880,6 @@
8398
  "node": ">=10.0.0"
8399
  }
8400
  },
8401
- "node_modules/strict-event-emitter": {
8402
- "version": "0.4.6",
8403
- "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz",
8404
- "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==",
8405
- "license": "MIT"
8406
- },
8407
  "node_modules/string.prototype.includes": {
8408
  "version": "2.0.1",
8409
  "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -8539,12 +8015,6 @@
8539
  "url": "https://github.com/sponsors/sindresorhus"
8540
  }
8541
  },
8542
- "node_modules/style-mod": {
8543
- "version": "4.1.2",
8544
- "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
8545
- "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
8546
- "license": "MIT"
8547
- },
8548
  "node_modules/styled-jsx": {
8549
  "version": "5.1.6",
8550
  "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -8892,12 +8362,6 @@
8892
  "url": "https://github.com/sponsors/Wombosvideo"
8893
  }
8894
  },
8895
- "node_modules/type": {
8896
- "version": "2.7.3",
8897
- "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
8898
- "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
8899
- "license": "ISC"
8900
- },
8901
  "node_modules/type-check": {
8902
  "version": "0.4.0",
8903
  "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -9181,12 +8645,6 @@
9181
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
9182
  }
9183
  },
9184
- "node_modules/w3c-keyname": {
9185
- "version": "2.2.8",
9186
- "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
9187
- "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
9188
- "license": "MIT"
9189
- },
9190
  "node_modules/watchpack": {
9191
  "version": "2.4.4",
9192
  "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
 
8
  "name": "deepsite-v2",
9
  "version": "0.1.0",
10
  "dependencies": {
 
11
  "@huggingface/hub": "^2.2.0",
12
  "@huggingface/inference": "^4.0.3",
13
  "@monaco-editor/react": "^4.7.0-rc.0",
 
97
  "node": ">=6.9.0"
98
  }
99
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  "node_modules/@emnapi/core": {
101
  "version": "1.4.3",
102
  "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
 
876
  "@jridgewell/sourcemap-codec": "^1.4.14"
877
  }
878
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
879
  "node_modules/@monaco-editor/loader": {
880
  "version": "1.5.0",
881
  "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
 
1110
  "node": ">=12.4.0"
1111
  }
1112
  },
 
 
 
 
 
 
1113
  "node_modules/@radix-ui/number": {
1114
  "version": "1.1.1",
1115
  "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
 
2055
  "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
2056
  "license": "MIT"
2057
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2058
  "node_modules/@rtsao/scc": {
2059
  "version": "1.1.0",
2060
  "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
 
2069
  "dev": true,
2070
  "license": "MIT"
2071
  },
 
 
 
 
 
 
2072
  "node_modules/@swc/counter": {
2073
  "version": "0.1.3",
2074
  "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
 
3341
  "ajv": "^6.9.1"
3342
  }
3343
  },
 
 
 
 
 
 
3344
  "node_modules/ansi-styles": {
3345
  "version": "4.3.0",
3346
  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
 
3620
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
3621
  "license": "MIT"
3622
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3623
  "node_modules/big.js": {
3624
  "version": "5.2.2",
3625
  "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
 
3695
  "node": ">=16.20.1"
3696
  }
3697
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3698
  "node_modules/buffer-from": {
3699
  "version": "1.1.2",
3700
  "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
 
3847
  "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
3848
  "license": "MIT"
3849
  },
 
 
 
 
 
 
3850
  "node_modules/client-only": {
3851
  "version": "0.0.1",
3852
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
 
3940
  "toggle-selection": "^1.0.6"
3941
  }
3942
  },
 
 
 
 
 
 
3943
  "node_modules/cross-spawn": {
3944
  "version": "7.0.6",
3945
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 
3982
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
3983
  "license": "MIT"
3984
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3985
  "node_modules/damerau-levenshtein": {
3986
  "version": "1.0.8",
3987
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
 
4121
  "node": ">=0.4.0"
4122
  }
4123
  },
 
 
 
 
 
 
 
 
 
4124
  "node_modules/detect-libc": {
4125
  "version": "2.0.4",
4126
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
 
4150
  "node": ">=0.10.0"
4151
  }
4152
  },
 
 
 
 
 
 
 
 
 
 
 
 
4153
  "node_modules/dunder-proto": {
4154
  "version": "1.0.1",
4155
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
 
4393
  "url": "https://github.com/sponsors/ljharb"
4394
  }
4395
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4396
  "node_modules/escalade": {
4397
  "version": "3.2.0",
4398
  "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
 
4404
  "node": ">=6"
4405
  }
4406
  },
 
 
 
 
 
 
4407
  "node_modules/escape-string-regexp": {
4408
  "version": "4.0.0",
4409
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
 
4775
  "url": "https://opencollective.com/eslint"
4776
  }
4777
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4778
  "node_modules/espree": {
4779
  "version": "10.3.0",
4780
  "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
 
4834
  "node": ">=0.10.0"
4835
  }
4836
  },
 
 
 
 
 
 
 
 
 
 
4837
  "node_modules/events": {
4838
  "version": "3.3.0",
4839
  "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
 
4845
  "node": ">=0.8.x"
4846
  }
4847
  },
 
 
 
 
 
 
 
 
 
4848
  "node_modules/fast-deep-equal": {
4849
  "version": "3.1.3",
4850
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 
5351
  "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
5352
  "license": "BSD-3-Clause"
5353
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5354
  "node_modules/ignore": {
5355
  "version": "5.3.2",
5356
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 
5409
  "node": ">= 0.4"
5410
  }
5411
  },
 
 
 
 
 
 
5412
  "node_modules/is-array-buffer": {
5413
  "version": "3.0.5",
5414
  "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
 
6340
  "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
6341
  }
6342
  },
 
 
 
 
 
 
 
 
 
6343
  "node_modules/magic-string": {
6344
  "version": "0.30.17",
6345
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
 
6726
  "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
6727
  }
6728
  },
 
 
 
 
 
 
6729
  "node_modules/next/node_modules/postcss": {
6730
  "version": "8.4.31",
6731
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
 
6902
  "node": ">= 0.8.0"
6903
  }
6904
  },
 
 
 
 
 
 
6905
  "node_modules/own-keys": {
6906
  "version": "1.0.1",
6907
  "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
 
7120
  "node": ">=0.10.0"
7121
  }
7122
  },
 
 
 
 
 
 
 
 
 
7123
  "node_modules/react-dom": {
7124
  "version": "19.1.0",
7125
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
 
7858
  "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
7859
  "license": "MIT"
7860
  },
 
 
 
 
 
 
 
 
 
 
 
 
7861
  "node_modules/stop-iteration-iterator": {
7862
  "version": "1.1.0",
7863
  "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
 
7880
  "node": ">=10.0.0"
7881
  }
7882
  },
 
 
 
 
 
 
7883
  "node_modules/string.prototype.includes": {
7884
  "version": "2.0.1",
7885
  "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
 
8015
  "url": "https://github.com/sponsors/sindresorhus"
8016
  }
8017
  },
 
 
 
 
 
 
8018
  "node_modules/styled-jsx": {
8019
  "version": "5.1.6",
8020
  "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
 
8362
  "url": "https://github.com/sponsors/Wombosvideo"
8363
  }
8364
  },
 
 
 
 
 
 
8365
  "node_modules/type-check": {
8366
  "version": "0.4.0",
8367
  "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
 
8645
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
8646
  }
8647
  },
 
 
 
 
 
 
8648
  "node_modules/watchpack": {
8649
  "version": "2.4.4",
8650
  "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
package.json CHANGED
@@ -9,7 +9,6 @@
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
- "@codesandbox/sandpack-react": "^2.20.0",
13
  "@huggingface/hub": "^2.2.0",
14
  "@huggingface/inference": "^4.0.3",
15
  "@monaco-editor/react": "^4.7.0-rc.0",
 
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
 
12
  "@huggingface/hub": "^2.2.0",
13
  "@huggingface/inference": "^4.0.3",
14
  "@monaco-editor/react": "^4.7.0-rc.0",
types/index.ts CHANGED
@@ -9,7 +9,7 @@ export interface User {
9
  }
10
 
11
  export interface HtmlHistory {
12
- pages: Page[];
13
  createdAt: Date;
14
  prompt: string;
15
  }
@@ -24,8 +24,3 @@ export interface Project {
24
  _updatedAt?: Date;
25
  _createdAt?: Date;
26
  }
27
-
28
- export interface Page {
29
- path: string;
30
- html: string;
31
- }
 
9
  }
10
 
11
  export interface HtmlHistory {
12
+ html: string;
13
  createdAt: Date;
14
  prompt: string;
15
  }
 
24
  _updatedAt?: Date;
25
  _createdAt?: Date;
26
  }