apply-changes-follow-up

#87
by enzostvs HF Staff - opened
Files changed (2) hide show
  1. server.js +372 -59
  2. src/components/ask-ai/ask-ai.tsx +71 -22
server.js CHANGED
@@ -12,6 +12,7 @@ import {
12
  } from "@huggingface/hub";
13
  import { InferenceClient } from "@huggingface/inference";
14
  import bodyParser from "body-parser";
 
15
 
16
  import checkUser from "./middlewares/checkUser.js";
17
  import { PROVIDERS } from "./utils/providers.js";
@@ -31,7 +32,7 @@ const PORT = process.env.APP_PORT || 3000;
31
  const REDIRECT_URI =
32
  process.env.REDIRECT_URI || `http://localhost:${PORT}/auth/login`;
33
  const MODEL_ID = "deepseek-ai/DeepSeek-V3-0324";
34
- const MAX_REQUESTS_PER_IP = 2;
35
 
36
  app.use(cookieParser());
37
  app.use(bodyParser.json());
@@ -212,6 +213,264 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
212
  }
213
  });
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  app.post("/api/ask-ai", async (req, res) => {
216
  const { prompt, html, previousPrompt, provider } = req.body;
217
  if (!prompt) {
@@ -228,6 +487,9 @@ app.post("/api/ask-ai", async (req, res) => {
228
  token = process.env.HF_TOKEN;
229
  }
230
 
 
 
 
231
  const ip =
232
  req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
233
  req.headers["x-real-ip"] ||
@@ -235,7 +497,7 @@ app.post("/api/ask-ai", async (req, res) => {
235
  req.ip ||
236
  "0.0.0.0";
237
 
238
- if (!token) {
239
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
240
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
241
  return res.status(429).send({
@@ -248,10 +510,56 @@ app.post("/api/ask-ai", async (req, res) => {
248
  token = process.env.DEFAULT_HF_TOKEN;
249
  }
250
 
251
- // Set up response headers for streaming
252
- res.setHeader("Content-Type", "text/plain");
253
- res.setHeader("Cache-Control", "no-cache");
254
- res.setHeader("Connection", "keep-alive");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  const client = new InferenceClient(token);
257
  let completeResponse = "";
@@ -274,72 +582,78 @@ app.post("/api/ask-ai", async (req, res) => {
274
  });
275
  }
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  try {
 
 
 
 
 
278
  const chatCompletion = client.chatCompletionStream({
279
  model: MODEL_ID,
280
  provider: selectedProvider.id,
281
- messages: [
282
- {
283
- role: "system",
284
- content: `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. 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`,
285
- },
286
- ...(previousPrompt
287
- ? [
288
- {
289
- role: "user",
290
- content: previousPrompt,
291
- },
292
- ]
293
- : []),
294
- ...(html
295
- ? [
296
- {
297
- role: "assistant",
298
- content: `The current code is: ${html}.`,
299
- },
300
- ]
301
- : []),
302
- {
303
- role: "user",
304
- content: prompt,
305
- },
306
- ],
307
  ...(selectedProvider.id !== "sambanova"
308
  ? {
309
  max_tokens: selectedProvider.max_tokens,
310
  }
311
  : {}),
 
312
  });
313
 
314
- while (true) {
315
- const { done, value } = await chatCompletion.next();
316
- if (done) {
317
- break;
318
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  const chunk = value.choices[0]?.delta?.content;
320
  if (chunk) {
321
- if (provider !== "sambanova") {
322
- res.write(chunk);
323
- completeResponse += chunk;
324
-
325
- if (completeResponse.includes("</html>")) {
326
- break;
327
- }
328
- } else {
329
- let newChunk = chunk;
330
- if (chunk.includes("</html>")) {
331
- // Replace everything after the last </html> tag with an empty string
332
- newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
333
- }
334
- completeResponse += newChunk;
335
- res.write(newChunk);
336
- if (newChunk.includes("</html>")) {
337
- break;
338
- }
339
- }
340
  }
341
  }
342
- // End the response stream
343
  res.end();
344
  } catch (error) {
345
  if (error.message.includes("exceeded your monthly included credits")) {
@@ -352,8 +666,7 @@ app.post("/api/ask-ai", async (req, res) => {
352
  if (!res.headersSent) {
353
  res.status(500).send({
354
  ok: false,
355
- message:
356
- error.message || "An error occurred while processing your request.",
357
  });
358
  } else {
359
  // Otherwise end the stream
 
12
  } from "@huggingface/hub";
13
  import { InferenceClient } from "@huggingface/inference";
14
  import bodyParser from "body-parser";
15
+ import { diff_match_patch } from "diff-match-patch"; // Using a library for robustness
16
 
17
  import checkUser from "./middlewares/checkUser.js";
18
  import { PROVIDERS } from "./utils/providers.js";
 
32
  const REDIRECT_URI =
33
  process.env.REDIRECT_URI || `http://localhost:${PORT}/auth/login`;
34
  const MODEL_ID = "deepseek-ai/DeepSeek-V3-0324";
35
+ const MAX_REQUESTS_PER_IP = 1000;
36
 
37
  app.use(cookieParser());
38
  app.use(bodyParser.json());
 
213
  }
214
  });
215
 
216
+ // --- Diff Parsing and Applying Logic ---
217
+
218
+ const SEARCH_START = "<<<<<<< SEARCH";
219
+ const DIVIDER = "=======";
220
+ const REPLACE_END = ">>>>>>> REPLACE";
221
+
222
+ /**
223
+ * Parses AI response content for SEARCH/REPLACE blocks.
224
+ * @param {string} content - The AI response content.
225
+ * @returns {Array<{original: string, updated: string}>} - Array of diff blocks.
226
+ */
227
+ function parseDiffBlocks(content) {
228
+ const blocks = [];
229
+ const lines = content.split("\n");
230
+ let i = 0;
231
+ while (i < lines.length) {
232
+ // Trim lines for comparison to handle potential trailing whitespace from AI
233
+ if (lines[i].trim() === SEARCH_START) {
234
+ const originalLines = [];
235
+ const updatedLines = [];
236
+ i++; // Move past SEARCH_START
237
+ while (i < lines.length && lines[i].trim() !== DIVIDER) {
238
+ originalLines.push(lines[i]);
239
+ i++;
240
+ }
241
+ if (i >= lines.length || lines[i].trim() !== DIVIDER) {
242
+ console.warn(
243
+ "Malformed diff block: Missing or misplaced '=======' after SEARCH block. Block content:",
244
+ originalLines.join("\n")
245
+ );
246
+ // Skip to next potential block start or end
247
+ while (i < lines.length && !lines[i].includes(SEARCH_START)) i++;
248
+ continue;
249
+ }
250
+ i++; // Move past DIVIDER
251
+ while (i < lines.length && lines[i].trim() !== REPLACE_END) {
252
+ updatedLines.push(lines[i]);
253
+ i++;
254
+ }
255
+ if (i >= lines.length || lines[i].trim() !== REPLACE_END) {
256
+ console.warn(
257
+ "Malformed diff block: Missing or misplaced '>>>>>>> REPLACE' after REPLACE block. Block content:",
258
+ updatedLines.join("\n")
259
+ );
260
+ // Skip to next potential block start or end
261
+ while (i < lines.length && !lines[i].includes(SEARCH_START)) i++;
262
+ continue;
263
+ }
264
+ // Important: Re-add newline characters lost during split('\n')
265
+ // Only add trailing newline if it wasn't the *very last* line of the block content before split
266
+ const originalText = originalLines.join("\n");
267
+ const updatedText = updatedLines.join("\n");
268
+
269
+ blocks.push({
270
+ original: originalText, // Don't add trailing newline here, handle in apply
271
+ updated: updatedText,
272
+ });
273
+ }
274
+ i++;
275
+ }
276
+ return blocks;
277
+ }
278
+
279
+ /**
280
+ * Applies a single diff block to the current HTML content using diff-match-patch.
281
+ * @param {string} currentHtml - The current HTML content.
282
+ * @param {string} originalBlock - The content from the SEARCH block.
283
+ * @param {string} updatedBlock - The content from the REPLACE block.
284
+ * @returns {string | null} - The updated HTML content, or null if patching failed.
285
+ */
286
+ function applySingleDiffFuzzy(currentHtml, originalBlock, updatedBlock) {
287
+ const dmp = new diff_match_patch();
288
+
289
+ // Handle potential trailing newline inconsistencies between AI and actual file
290
+ // If originalBlock doesn't end with newline but exists in currentHtml *with* one, add it.
291
+ let searchBlock = originalBlock;
292
+ if (
293
+ !originalBlock.endsWith("\n") &&
294
+ currentHtml.includes(originalBlock + "\n")
295
+ ) {
296
+ searchBlock = originalBlock + "\n";
297
+ }
298
+ // If updatedBlock is meant to replace a block ending in newline, ensure it also does (unless empty)
299
+ let replaceBlock = updatedBlock;
300
+ if (
301
+ searchBlock.endsWith("\n") &&
302
+ updatedBlock.length > 0 &&
303
+ !updatedBlock.endsWith("\n")
304
+ ) {
305
+ replaceBlock = updatedBlock + "\n";
306
+ }
307
+ // If deleting a block ending in newline, the replacement is empty
308
+ if (searchBlock.endsWith("\n") && updatedBlock.length === 0) {
309
+ replaceBlock = "";
310
+ }
311
+
312
+ // 1. Create a patch from the (potentially adjusted) original and updated blocks
313
+ const patchText = dmp.patch_make(searchBlock, replaceBlock);
314
+
315
+ // 2. Apply the patch to the current HTML
316
+ // diff-match-patch is good at finding the location even with slight context variations.
317
+ // Increase Match_Threshold for potentially larger files or more significant context drift.
318
+ dmp.Match_Threshold = 0.6; // Adjust as needed (0.0 to 1.0)
319
+ dmp.Patch_DeleteThreshold = 0.6; // Adjust as needed
320
+ const [patchedHtml, results] = dmp.patch_apply(patchText, currentHtml);
321
+
322
+ // 3. Check if the patch applied successfully
323
+ if (results.every((result) => result === true)) {
324
+ return patchedHtml;
325
+ } else {
326
+ console.warn(
327
+ "Patch application failed using diff-match-patch. Results:",
328
+ results
329
+ );
330
+ // Fallback: Try exact string replacement (less robust)
331
+ if (currentHtml.includes(searchBlock)) {
332
+ console.log("Falling back to direct string replacement.");
333
+ // Use replace only once
334
+ const index = currentHtml.indexOf(searchBlock);
335
+ if (index !== -1) {
336
+ return (
337
+ currentHtml.substring(0, index) +
338
+ replaceBlock +
339
+ currentHtml.substring(index + searchBlock.length)
340
+ );
341
+ }
342
+ }
343
+ console.error("Direct string replacement fallback also failed.");
344
+ return null; // Indicate failure
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Applies all parsed diff blocks sequentially to the original HTML.
350
+ * @param {string} originalHtml - The initial HTML content.
351
+ * @param {string} aiResponseContent - The full response from the AI containing diff blocks.
352
+ * @returns {string} - The final modified HTML.
353
+ * @throws {Error} If any diff block fails to apply.
354
+ */
355
+ function applyDiffs(originalHtml, aiResponseContent) {
356
+ const diffBlocks = parseDiffBlocks(aiResponseContent);
357
+
358
+ if (diffBlocks.length === 0) {
359
+ console.warn("AI response did not contain valid SEARCH/REPLACE blocks.");
360
+ // Check if the AI *tried* to use the format but failed, or just gave full code
361
+ if (
362
+ aiResponseContent.includes(SEARCH_START) ||
363
+ aiResponseContent.includes(DIVIDER) ||
364
+ aiResponseContent.includes(REPLACE_END)
365
+ ) {
366
+ throw new Error(
367
+ "AI response contained malformed or unparseable diff blocks. Could not apply changes."
368
+ );
369
+ }
370
+ // If no diff blocks *at all*, maybe the AI ignored the instruction and gave full code?
371
+ // Heuristic: If the response looks like a full HTML doc, use it directly.
372
+ const trimmedResponse = aiResponseContent.trim().toLowerCase();
373
+ if (
374
+ trimmedResponse.startsWith("<!doctype html") ||
375
+ trimmedResponse.startsWith("<html")
376
+ ) {
377
+ console.warn(
378
+ "[Diff Apply] AI response seems to be full HTML despite diff instructions. Using full response as fallback."
379
+ );
380
+ return aiResponseContent;
381
+ }
382
+ console.warn(
383
+ "[Diff Apply] No valid diff blocks found and response doesn't look like full HTML. Returning original HTML."
384
+ );
385
+ return originalHtml; // Return original if no diffs and not full HTML
386
+ }
387
+
388
+ console.log(`Found ${diffBlocks.length} diff blocks to apply.`);
389
+ let currentHtml = originalHtml;
390
+ for (let i = 0; i < diffBlocks.length; i++) {
391
+ const { original, updated } = diffBlocks[i];
392
+ console.log(`Applying block ${i + 1}...`);
393
+ const result = applySingleDiffFuzzy(currentHtml, original, updated);
394
+
395
+ if (result === null) {
396
+ // Log detailed error for debugging
397
+ console.error(`Failed to apply diff block ${i + 1}:`);
398
+ console.error("--- SEARCH ---");
399
+ console.error(original);
400
+ console.error("--- REPLACE ---");
401
+ console.error(updated);
402
+ console.error("--- CURRENT CONTEXT (approx) ---");
403
+ // Try finding the first line of the original block for context
404
+ const firstLine = original.split("\n")[0];
405
+ let contextIndex = -1;
406
+ if (firstLine) {
407
+ contextIndex = currentHtml.indexOf(firstLine);
408
+ }
409
+ if (contextIndex === -1) {
410
+ // If first line not found, maybe try middle line?
411
+ const lines = original.split("\n");
412
+ if (lines.length > 2) {
413
+ contextIndex = currentHtml.indexOf(
414
+ lines[Math.floor(lines.length / 2)]
415
+ );
416
+ }
417
+ }
418
+ if (contextIndex === -1) {
419
+ // Still not found, just show start
420
+ contextIndex = 0;
421
+ }
422
+
423
+ console.error(
424
+ currentHtml.substring(
425
+ Math.max(0, contextIndex - 150),
426
+ Math.min(currentHtml.length, contextIndex + original.length + 300)
427
+ )
428
+ );
429
+ console.error("---------------------------------");
430
+
431
+ throw new Error(
432
+ `Failed to apply AI-suggested change ${
433
+ i + 1
434
+ }. The 'SEARCH' block might not accurately match the current code.`
435
+ );
436
+ }
437
+ currentHtml = result;
438
+ }
439
+
440
+ console.log("All diff blocks applied successfully.");
441
+ return currentHtml;
442
+ }
443
+
444
+ // --- Endpoint to Apply Diffs Server-Side ---
445
+ app.post("/api/apply-diffs", (req, res) => {
446
+ const { originalHtml, aiResponseContent } = req.body;
447
+
448
+ if (
449
+ typeof originalHtml !== "string" ||
450
+ typeof aiResponseContent !== "string"
451
+ ) {
452
+ return res.status(400).json({
453
+ ok: false,
454
+ message: "Missing or invalid originalHtml or aiResponseContent.",
455
+ });
456
+ }
457
+
458
+ try {
459
+ console.log("[Apply Diffs] Received request to apply diffs.");
460
+ const modifiedHtml = applyDiffs(originalHtml, aiResponseContent);
461
+ console.log("[Apply Diffs] Diffs applied successfully.");
462
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
463
+ res.status(200).send(modifiedHtml);
464
+ } catch (error) {
465
+ console.error("[Apply Diffs] Error applying diffs:", error);
466
+ res.status(400).json({
467
+ // Use 400 for client-side correctable errors (bad diff format)
468
+ ok: false,
469
+ message: error.message || "Failed to apply AI suggestions.",
470
+ });
471
+ }
472
+ });
473
+
474
  app.post("/api/ask-ai", async (req, res) => {
475
  const { prompt, html, previousPrompt, provider } = req.body;
476
  if (!prompt) {
 
487
  token = process.env.HF_TOKEN;
488
  }
489
 
490
+ const isFollowUp = !!html;
491
+ console.log(`[AI Request] Type: ${isFollowUp ? "Follow-up" : "Initial"}`);
492
+
493
  const ip =
494
  req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
495
  req.headers["x-real-ip"] ||
 
497
  req.ip ||
498
  "0.0.0.0";
499
 
500
+ if (!hf_token) {
501
  ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
502
  if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
503
  return res.status(429).send({
 
510
  token = process.env.DEFAULT_HF_TOKEN;
511
  }
512
 
513
+ // --- Define System Prompts ---
514
+ const initialSystemPrompt = `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. Also, try to ellaborate as much as you can, to create something unique. If needed you are allowed to use tailwincss (if so make sure to import <script src="https://cdn.tailwindcss.com"></script> in the head). ALWAYS GIVE THE RESPONSE INTO A SINGLE HTML FILE.`;
515
+ const followUpSystemPrompt = `You are an expert web developer modifying an existing HTML file.
516
+ The user wants to apply changes based on their request.
517
+ You MUST output ONLY the changes required using the following SEARCH/REPLACE block format. Do NOT output the entire file.
518
+ Explain the changes briefly *before* the blocks if necessary, but the code changes THEMSELVES MUST be within the blocks.
519
+ Format Rules:
520
+ 1. Start with ${SEARCH_START}
521
+ 2. Provide the exact lines from the current code that need to be replaced.
522
+ 3. Use ${DIVIDER} to separate the search block from the replacement.
523
+ 4. Provide the new lines that should replace the original lines.
524
+ 5. End with ${REPLACE_END}
525
+ 6. You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the file.
526
+ 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.
527
+ 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).
528
+ 9. IMPORTANT: The SEARCH block must *exactly* match the current code, including indentation and whitespace.
529
+ Example Modifying Code:
530
+ \`\`\`
531
+ Some explanation...
532
+ ${SEARCH_START}
533
+ <h1>Old Title</h1>
534
+ ${DIVIDER}
535
+ <h1>New Title</h1>
536
+ ${REPLACE_END}
537
+ ${SEARCH_START}
538
+ </body>
539
+ ${DIVIDER}
540
+ <script>console.log("Added script");</script>
541
+ </body>
542
+ ${REPLACE_END}
543
+ \`\`\`
544
+ Example Deleting Code:
545
+ \`\`\`
546
+ Removing the paragraph...
547
+ ${SEARCH_START}
548
+ <p>This paragraph will be deleted.</p>
549
+ ${DIVIDER}
550
+ ${REPLACE_END}
551
+ \`\`\`
552
+ ONLY output the changes in this format. Do NOT output the full HTML file again.`;
553
+
554
+ // --- Prepare Messages for AI ---
555
+ const systemPromptContent = isFollowUp
556
+ ? followUpSystemPrompt
557
+ : initialSystemPrompt;
558
+ console.log(
559
+ `[AI Request] Using system prompt: ${
560
+ isFollowUp ? "Follow-up (Diff)" : "Initial (Full HTML)"
561
+ }`
562
+ );
563
 
564
  const client = new InferenceClient(token);
565
  let completeResponse = "";
 
582
  });
583
  }
584
 
585
+ const messages = [
586
+ {
587
+ role: "system",
588
+ content: systemPromptContent,
589
+ },
590
+ ...(previousPrompt ? [{ role: "user", content: previousPrompt }] : []),
591
+ ...(isFollowUp
592
+ ? [
593
+ {
594
+ role: "assistant",
595
+ content: `Okay, I have the current code. It is:\n\`\`\`html\n${html}\n\`\`\``,
596
+ },
597
+ ]
598
+ : []),
599
+ { role: "user", content: prompt },
600
+ ];
601
+
602
  try {
603
+ res.setHeader("Content-Type", "text/plain; charset=utf-8"); // Stream raw text
604
+ res.setHeader("Cache-Control", "no-cache");
605
+ res.setHeader("Connection", "keep-alive");
606
+ res.setHeader("X-Response-Type", isFollowUp ? "diff" : "full"); // Signal type to client
607
+
608
  const chatCompletion = client.chatCompletionStream({
609
  model: MODEL_ID,
610
  provider: selectedProvider.id,
611
+ messages,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  ...(selectedProvider.id !== "sambanova"
613
  ? {
614
  max_tokens: selectedProvider.max_tokens,
615
  }
616
  : {}),
617
+ temperature: isFollowUp ? 0 : undefined,
618
  });
619
 
620
+ console.log("[AI Request] Starting stream to client...");
621
+ // while (true) {
622
+ // const { done, value } = await chatCompletion.next();
623
+ // if (done) {
624
+ // break;
625
+ // }
626
+ // const chunk = value.choices[0]?.delta?.content;
627
+ // if (chunk) {
628
+ // if (provider !== "sambanova") {
629
+ // res.write(chunk);
630
+ // completeResponse += chunk;
631
+
632
+ // if (completeResponse.includes("</html>")) {
633
+ // break;
634
+ // }
635
+ // } else {
636
+ // let newChunk = chunk;
637
+ // if (chunk.includes("</html>")) {
638
+ // // Replace everything after the last </html> tag with an empty string
639
+ // newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
640
+ // }
641
+ // completeResponse += newChunk;
642
+ // res.write(newChunk);
643
+ // if (newChunk.includes("</html>")) {
644
+ // break;
645
+ // }
646
+ // }
647
+ // }
648
+ // }
649
+ for await (const value of chatCompletion) {
650
  const chunk = value.choices[0]?.delta?.content;
651
  if (chunk) {
652
+ res.write(chunk); // Stream raw AI response chunk
653
+ completeResponse += chunk; // Accumulate for logging completion
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  }
655
  }
656
+ console.log("[AI Request] Stream finished.");
657
  res.end();
658
  } catch (error) {
659
  if (error.message.includes("exceeded your monthly included credits")) {
 
666
  if (!res.headersSent) {
667
  res.status(500).send({
668
  ok: false,
669
+ message: `Error processing AI request: ${error.message}. You might need to start a new conversation by refreshing the page.`,
 
670
  });
671
  } else {
672
  // Otherwise end the stream
src/components/ask-ai/ask-ai.tsx CHANGED
@@ -45,10 +45,12 @@ function AskAI({
45
 
46
  const callAi = async () => {
47
  if (isAiWorking || !prompt.trim()) return;
 
48
  setisAiWorking(true);
49
  setProviderError("");
50
 
51
  let contentResponse = "";
 
52
  let lastRenderTime = 0;
53
  try {
54
  onNewPrompt(prompt);
@@ -80,6 +82,9 @@ function AskAI({
80
  setisAiWorking(false);
81
  return;
82
  }
 
 
 
83
  const reader = request.body.getReader();
84
  const decoder = new TextDecoder("utf-8");
85
 
@@ -94,36 +99,80 @@ function AskAI({
94
  audio.play();
95
  setView("preview");
96
 
97
- // Now we have the complete HTML including </html>, so set it to be sure
98
- const finalDoc = contentResponse.match(
99
- /<!DOCTYPE html>[\s\S]*<\/html>/
100
- )?.[0];
101
- if (finalDoc) {
102
- setHtml(finalDoc);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
 
105
  return;
106
  }
107
 
108
  const chunk = decoder.decode(value, { stream: true });
109
- contentResponse += chunk;
110
- const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
111
- if (newHtml) {
112
- // Force-close the HTML tag so the iframe doesn't render half-finished markup
113
- let partialDoc = newHtml;
114
- if (!partialDoc.includes("</html>")) {
115
- partialDoc += "\n</html>";
116
- }
 
 
 
 
 
 
117
 
118
- // Throttle the re-renders to avoid flashing/flicker
119
- const now = Date.now();
120
- if (now - lastRenderTime > 300) {
121
- setHtml(partialDoc);
122
- lastRenderTime = now;
123
- }
124
 
125
- if (partialDoc.length > 200) {
126
- onScrollToBottom();
 
127
  }
128
  }
129
  read();
 
45
 
46
  const callAi = async () => {
47
  if (isAiWorking || !prompt.trim()) return;
48
+ const originalHtml = html; // Store the HTML state at the start of the request
49
  setisAiWorking(true);
50
  setProviderError("");
51
 
52
  let contentResponse = "";
53
+ let accumulatedDiffResponse = "";
54
  let lastRenderTime = 0;
55
  try {
56
  onNewPrompt(prompt);
 
82
  setisAiWorking(false);
83
  return;
84
  }
85
+ const responseType = request.headers.get("X-Response-Type") || "full"; // Default to full if header missing
86
+ console.log(`[AI Response] Type: ${responseType}`);
87
+
88
  const reader = request.body.getReader();
89
  const decoder = new TextDecoder("utf-8");
90
 
 
99
  audio.play();
100
  setView("preview");
101
 
102
+ if (responseType === "diff") {
103
+ // Apply diffs server-side
104
+ try {
105
+ console.log(
106
+ "[Diff Apply] Sending original HTML and AI diff response to server..."
107
+ );
108
+ const applyRequest = await fetch("/api/apply-diffs", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({
112
+ originalHtml: originalHtml, // Send the HTML from the start of the request
113
+ aiResponseContent: accumulatedDiffResponse,
114
+ }),
115
+ });
116
+
117
+ if (!applyRequest.ok) {
118
+ const errorData = await applyRequest.json();
119
+ throw new Error(
120
+ errorData.message ||
121
+ `Server failed to apply diffs (status ${applyRequest.status})`
122
+ );
123
+ }
124
+
125
+ const patchedHtml = await applyRequest.text();
126
+ console.log("[Diff Apply] Received patched HTML from server.");
127
+ setHtml(patchedHtml); // Update editor with the final result
128
+ toast.success("AI changes applied");
129
+ } catch (applyError: any) {
130
+ console.error("Error applying diffs server-side:", applyError);
131
+ toast.error(
132
+ `Failed to apply AI changes: ${applyError.message}`
133
+ );
134
+ // Optionally revert to originalHtml? Or leave the editor as is?
135
+ // setHtml(originalHtml); // Uncomment to revert on failure
136
+ }
137
+ } else {
138
+ // Now we have the complete HTML including </html>, so set it to be sure
139
+ const finalDoc = contentResponse.match(
140
+ /<!DOCTYPE html>[\s\S]*<\/html>/
141
+ )?.[0];
142
+ if (finalDoc) {
143
+ setHtml(finalDoc);
144
+ }
145
  }
146
 
147
  return;
148
  }
149
 
150
  const chunk = decoder.decode(value, { stream: true });
151
+ if (responseType === "diff") {
152
+ // --- Diff Mode ---
153
+ accumulatedDiffResponse += chunk; // Just accumulate the raw response
154
+ } else {
155
+ contentResponse += chunk;
156
+ const newHtml = contentResponse.match(
157
+ /<!DOCTYPE html>[\s\S]*/
158
+ )?.[0];
159
+ if (newHtml) {
160
+ // Force-close the HTML tag so the iframe doesn't render half-finished markup
161
+ let partialDoc = newHtml;
162
+ if (!partialDoc.includes("</html>")) {
163
+ partialDoc += "\n</html>";
164
+ }
165
 
166
+ // Throttle the re-renders to avoid flashing/flicker
167
+ const now = Date.now();
168
+ if (now - lastRenderTime > 300) {
169
+ setHtml(partialDoc);
170
+ lastRenderTime = now;
171
+ }
172
 
173
+ if (partialDoc.length > 200) {
174
+ onScrollToBottom();
175
+ }
176
  }
177
  }
178
  read();