Spaces:
markush1
/
Running on CPU Upgrade

mhupfauer commited on
Commit
caae608
·
2 Parent(s): 0dfdfdb 4240661

Merge remote-tracking branch 'upstream/main'

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