errors-forwardings

#290
by victor HF Staff - opened
app/api/ask-ai/route.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
  SEARCH_START,
15
  } from "@/lib/prompts";
16
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
 
17
 
18
  const ipAddresses = new Map();
19
 
@@ -223,8 +224,15 @@ export async function PUT(request: NextRequest) {
223
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
224
 
225
  const body = await request.json();
226
- const { prompt, html, previousPrompt, provider, selectedElementHtml, model } =
227
- body;
 
 
 
 
 
 
 
228
 
229
  if (!prompt || !html) {
230
  return NextResponse.json(
@@ -243,6 +251,16 @@ export async function PUT(request: NextRequest) {
243
  );
244
  }
245
 
 
 
 
 
 
 
 
 
 
 
246
  let token = userToken;
247
  let billTo: string | null = null;
248
 
@@ -311,7 +329,10 @@ export async function PUT(request: NextRequest) {
311
  },
312
  {
313
  role: "user",
314
- content: prompt,
 
 
 
315
  },
316
  ],
317
  ...(selectedProvider.id !== "sambanova"
 
14
  SEARCH_START,
15
  } from "@/lib/prompts";
16
  import MY_TOKEN_KEY from "@/lib/get-cookie-name";
17
+ import { createErrorFixPrompt } from "@/lib/error-formatter";
18
 
19
  const ipAddresses = new Map();
20
 
 
224
  const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
225
 
226
  const body = await request.json();
227
+ const {
228
+ prompt,
229
+ html,
230
+ previousPrompt,
231
+ provider,
232
+ selectedElementHtml,
233
+ model,
234
+ errors,
235
+ } = body;
236
 
237
  if (!prompt || !html) {
238
  return NextResponse.json(
 
251
  );
252
  }
253
 
254
+ if (process.env.NODE_ENV !== "production") {
255
+ // Sanitize error messages to prevent log injection
256
+ const errorCount = errors && Array.isArray(errors) ? errors.length : 0;
257
+ console.log(
258
+ `[PUT /api/ask-ai] Model: ${selectedModel.label}, Provider: ${provider}${
259
+ errorCount > 0 ? `, Fixing ${errorCount} errors` : ""
260
+ }`
261
+ );
262
+ }
263
+
264
  let token = userToken;
265
  let billTo: string | null = null;
266
 
 
329
  },
330
  {
331
  role: "user",
332
+ content:
333
+ errors && errors.length > 0
334
+ ? createErrorFixPrompt(errors, html)
335
+ : prompt,
336
  },
337
  ],
338
  ...(selectedProvider.id !== "sambanova"
components/editor/ask-ai/index.tsx CHANGED
@@ -22,6 +22,23 @@ 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,
@@ -35,6 +52,7 @@ export function AskAI({
35
  setIsEditableModeEnabled,
36
  onNewPrompt,
37
  onSuccess,
 
38
  }: {
39
  html: string;
40
  setHtml: (html: string) => void;
@@ -48,6 +66,7 @@ export function AskAI({
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);
@@ -66,14 +85,21 @@ export function AskAI({
66
  const [isThinking, setIsThinking] = useState(true);
67
  const [controller, setController] = useState<AbortController | null>(null);
68
  const [isFollowUp, setIsFollowUp] = useState(true);
 
 
 
 
 
 
69
 
70
  const selectedModel = useMemo(() => {
71
  return MODELS.find((m: { value: string }) => m.value === model);
72
  }, [model]);
73
 
74
- const callAi = async (redesignMarkdown?: string) => {
75
  if (isAiWorking) return;
76
- if (!redesignMarkdown && !prompt.trim()) return;
 
77
  setisAiWorking(true);
78
  setProviderError("");
79
  setThink("");
@@ -95,12 +121,16 @@ export function AskAI({
95
  const request = await fetch("/api/ask-ai", {
96
  method: "PUT",
97
  body: JSON.stringify({
98
- prompt,
 
 
 
99
  provider,
100
  previousPrompt,
101
  model,
102
  html,
103
  selectedElementHtml,
 
104
  }),
105
  headers: {
106
  "Content-Type": "application/json",
@@ -129,7 +159,12 @@ export function AskAI({
129
  setPreviousPrompt(prompt);
130
  setPrompt("");
131
  setisAiWorking(false);
132
- onSuccess(res.html, prompt, res.updatedLines);
 
 
 
 
 
133
  if (audio.current) audio.current.play();
134
  }
135
  } else {
@@ -152,7 +187,7 @@ export function AskAI({
152
  const reader = request.body.getReader();
153
  const decoder = new TextDecoder("utf-8");
154
  const selectedModel = MODELS.find(
155
- (m: { value: string }) => m.value === model
156
  );
157
  let contentThink: string | undefined = undefined;
158
  const read = async () => {
@@ -181,6 +216,7 @@ export function AskAI({
181
  setPreviousPrompt(prompt);
182
  setPrompt("");
183
  setisAiWorking(false);
 
184
  setHasAsked(true);
185
  if (selectedModel?.isThinker) {
186
  setModel(MODELS[0].value);
@@ -189,7 +225,7 @@ export function AskAI({
189
 
190
  // Now we have the complete HTML including </html>, so set it to be sure
191
  const finalDoc = contentResponse.match(
192
- /<!DOCTYPE html>[\s\S]*<\/html>/
193
  )?.[0];
194
  if (finalDoc) {
195
  setHtml(finalDoc);
@@ -216,7 +252,7 @@ export function AskAI({
216
  contentResponse += chunk;
217
 
218
  const newHtml = contentResponse.match(
219
- /<!DOCTYPE html>[\s\S]*/
220
  )?.[0];
221
  if (newHtml) {
222
  setIsThinking(false);
@@ -290,6 +326,58 @@ export function AskAI({
290
  return isTheSameHtml(html);
291
  }, [html]);
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  return (
294
  <div className="px-3">
295
  <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-10 w-full group">
@@ -309,7 +397,7 @@ export function AskAI({
309
  "size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
310
  {
311
  "rotate-180": openThink,
312
- }
313
  )}
314
  />
315
  </header>
@@ -321,7 +409,7 @@ export function AskAI({
321
  "max-h-[0px]": !openThink,
322
  "min-h-[250px] max-h-[250px] border-t border-neutral-700":
323
  openThink,
324
- }
325
  )}
326
  >
327
  <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
@@ -364,14 +452,14 @@ export function AskAI({
364
  "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
365
  {
366
  "!pt-2.5": selectedElement && !isAiWorking,
367
- }
368
  )}
369
  placeholder={
370
  selectedElement
371
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
372
  : hasAsked
373
- ? "Ask DeepSite for edits"
374
- : "Ask DeepSite anything..."
375
  }
376
  value={prompt}
377
  onChange={(e) => setPrompt(e.target.value)}
@@ -412,6 +500,36 @@ export function AskAI({
412
  </TooltipContent>
413
  </Tooltip>
414
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  <InviteFriends />
416
  </div>
417
  <div className="flex items-center justify-end gap-2">
 
22
  import { SelectedHtmlElement } from "./selected-html-element";
23
  import { FollowUpTooltip } from "./follow-up-tooltip";
24
  import { isTheSameHtml } from "@/lib/compare-html-diff";
25
+ import {
26
+ areErrorsFixable,
27
+ deduplicateErrors,
28
+ createErrorFixPrompt,
29
+ } from "@/lib/error-formatter";
30
+ import { AlertCircle } from "lucide-react";
31
+ import type { PreviewError } from "@/types/preview-error";
32
+ import { MAX_AUTO_FIX_ATTEMPTS, AUTO_FIX_DELAY_MS } from "@/lib/constants";
33
+
34
+ // Reset function for cleaning up state
35
+ export function resetAutoFixState(
36
+ setAutoFixEnabled: (value: boolean) => void,
37
+ setFixAttempts: (value: number) => void,
38
+ ) {
39
+ setAutoFixEnabled(false);
40
+ setFixAttempts(0);
41
+ }
42
 
43
  export function AskAI({
44
  html,
 
52
  setIsEditableModeEnabled,
53
  onNewPrompt,
54
  onSuccess,
55
+ previewErrors = [],
56
  }: {
57
  html: string;
58
  setHtml: (html: string) => void;
 
66
  setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
67
  selectedElement?: HTMLElement | null;
68
  setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
69
+ previewErrors?: PreviewError[];
70
  }) {
71
  const refThink = useRef<HTMLDivElement | null>(null);
72
  const audio = useRef<HTMLAudioElement | null>(null);
 
85
  const [isThinking, setIsThinking] = useState(true);
86
  const [controller, setController] = useState<AbortController | null>(null);
87
  const [isFollowUp, setIsFollowUp] = useState(true);
88
+ const [autoFixEnabled, setAutoFixEnabled] = useLocalStorage(
89
+ "autoFixEnabled",
90
+ false,
91
+ );
92
+ const [fixAttempts, setFixAttempts] = useState(0);
93
+ const [isFixingErrors, setIsFixingErrors] = useState(false);
94
 
95
  const selectedModel = useMemo(() => {
96
  return MODELS.find((m: { value: string }) => m.value === model);
97
  }, [model]);
98
 
99
+ const callAi = async (redesignMarkdown?: string, errors?: PreviewError[]) => {
100
  if (isAiWorking) return;
101
+ if (!redesignMarkdown && !prompt.trim() && (!errors || errors.length === 0))
102
+ return;
103
  setisAiWorking(true);
104
  setProviderError("");
105
  setThink("");
 
121
  const request = await fetch("/api/ask-ai", {
122
  method: "PUT",
123
  body: JSON.stringify({
124
+ prompt:
125
+ errors && errors.length > 0
126
+ ? createErrorFixPrompt(errors, html)
127
+ : prompt,
128
  provider,
129
  previousPrompt,
130
  model,
131
  html,
132
  selectedElementHtml,
133
+ errors,
134
  }),
135
  headers: {
136
  "Content-Type": "application/json",
 
159
  setPreviousPrompt(prompt);
160
  setPrompt("");
161
  setisAiWorking(false);
162
+ setIsFixingErrors(false);
163
+ onSuccess(
164
+ res.html,
165
+ errors ? "Fixed errors automatically" : prompt,
166
+ res.updatedLines,
167
+ );
168
  if (audio.current) audio.current.play();
169
  }
170
  } else {
 
187
  const reader = request.body.getReader();
188
  const decoder = new TextDecoder("utf-8");
189
  const selectedModel = MODELS.find(
190
+ (m: { value: string }) => m.value === model,
191
  );
192
  let contentThink: string | undefined = undefined;
193
  const read = async () => {
 
216
  setPreviousPrompt(prompt);
217
  setPrompt("");
218
  setisAiWorking(false);
219
+ setIsFixingErrors(false);
220
  setHasAsked(true);
221
  if (selectedModel?.isThinker) {
222
  setModel(MODELS[0].value);
 
225
 
226
  // Now we have the complete HTML including </html>, so set it to be sure
227
  const finalDoc = contentResponse.match(
228
+ /<!DOCTYPE html>[\s\S]*<\/html>/,
229
  )?.[0];
230
  if (finalDoc) {
231
  setHtml(finalDoc);
 
252
  contentResponse += chunk;
253
 
254
  const newHtml = contentResponse.match(
255
+ /<!DOCTYPE html>[\s\S]*/,
256
  )?.[0];
257
  if (newHtml) {
258
  setIsThinking(false);
 
326
  return isTheSameHtml(html);
327
  }, [html]);
328
 
329
+ // Process and deduplicate errors
330
+ const fixableErrors = useMemo(() => {
331
+ if (!previewErrors || previewErrors.length === 0) return [];
332
+ const dedupedErrors = deduplicateErrors(previewErrors);
333
+ return dedupedErrors.filter((error) => areErrorsFixable([error]));
334
+ }, [previewErrors]);
335
+
336
+ // Auto-fix errors when detected
337
+ useUpdateEffect(() => {
338
+ if (
339
+ autoFixEnabled &&
340
+ fixableErrors.length > 0 &&
341
+ !isAiWorking &&
342
+ !isFixingErrors &&
343
+ fixAttempts < MAX_AUTO_FIX_ATTEMPTS
344
+ ) {
345
+ // Add a small delay to avoid immediate re-triggering
346
+ const timer = setTimeout(() => {
347
+ setIsFixingErrors(true);
348
+ setFixAttempts((prev) => prev + 1);
349
+ toast.info(
350
+ `Auto-fixing ${fixableErrors.length} error${fixableErrors.length > 1 ? "s" : ""}...`,
351
+ );
352
+ callAi(undefined, fixableErrors);
353
+ }, AUTO_FIX_DELAY_MS);
354
+
355
+ return () => clearTimeout(timer);
356
+ } else if (
357
+ autoFixEnabled &&
358
+ fixableErrors.length > 0 &&
359
+ fixAttempts >= MAX_AUTO_FIX_ATTEMPTS &&
360
+ !isAiWorking
361
+ ) {
362
+ // Max attempts reached, notify user
363
+ toast.error(
364
+ `Failed to auto-fix after ${MAX_AUTO_FIX_ATTEMPTS} attempts. Please fix manually.`,
365
+ );
366
+ setAutoFixEnabled(false);
367
+ }
368
+ }, [fixableErrors, autoFixEnabled, isAiWorking, fixAttempts]);
369
+
370
+ // Reset fix attempts when errors are cleared
371
+ useUpdateEffect(() => {
372
+ if (fixableErrors.length === 0 && fixAttempts > 0) {
373
+ setFixAttempts(0);
374
+ setIsFixingErrors(false);
375
+ if (autoFixEnabled) {
376
+ toast.success("All errors fixed!");
377
+ }
378
+ }
379
+ }, [fixableErrors]);
380
+
381
  return (
382
  <div className="px-3">
383
  <div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-10 w-full group">
 
397
  "size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
398
  {
399
  "rotate-180": openThink,
400
+ },
401
  )}
402
  />
403
  </header>
 
409
  "max-h-[0px]": !openThink,
410
  "min-h-[250px] max-h-[250px] border-t border-neutral-700":
411
  openThink,
412
+ },
413
  )}
414
  >
415
  <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
 
452
  "w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
453
  {
454
  "!pt-2.5": selectedElement && !isAiWorking,
455
+ },
456
  )}
457
  placeholder={
458
  selectedElement
459
  ? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
460
  : hasAsked
461
+ ? "Ask DeepSite for edits"
462
+ : "Ask DeepSite anything..."
463
  }
464
  value={prompt}
465
  onChange={(e) => setPrompt(e.target.value)}
 
500
  </TooltipContent>
501
  </Tooltip>
502
  )}
503
+ {fixableErrors.length > 0 && (
504
+ <Tooltip>
505
+ <TooltipTrigger asChild>
506
+ <Button
507
+ size="xs"
508
+ variant={autoFixEnabled ? "destructive" : "outline"}
509
+ onClick={() => setAutoFixEnabled?.(!autoFixEnabled)}
510
+ className={classNames("h-[28px]", {
511
+ "!text-red-400 hover:!text-red-200 !border-red-600 !hover:!border-red-500":
512
+ !autoFixEnabled && fixableErrors.length > 0,
513
+ })}
514
+ >
515
+ <AlertCircle className="size-4" />
516
+ {fixableErrors.length} Error
517
+ {fixableErrors.length > 1 ? "s" : ""}
518
+ {autoFixEnabled &&
519
+ fixAttempts > 0 &&
520
+ ` (${fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS})`}
521
+ </Button>
522
+ </TooltipTrigger>
523
+ <TooltipContent
524
+ align="start"
525
+ className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
526
+ >
527
+ {autoFixEnabled
528
+ ? `Auto-fix is ON. Click to disable. ${fixAttempts > 0 ? `Attempted ${fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS} fixes.` : ""}`
529
+ : "Click to enable auto-fix for detected errors"}
530
+ </TooltipContent>
531
+ </Tooltip>
532
+ )}
533
  <InviteFriends />
534
  </div>
535
  <div className="flex items-center justify-end gap-2">
components/editor/index.tsx CHANGED
@@ -26,6 +26,7 @@ import { Project } from "@/types";
26
  import { SaveButton } from "./save-button";
27
  import { LoadProject } from "../my-projects/load-project";
28
  import { isTheSameHtml } from "@/lib/compare-html-diff";
 
29
 
30
  export const AppEditor = ({ project }: { project?: Project | null }) => {
31
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
@@ -51,8 +52,9 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
51
  const [isAiWorking, setIsAiWorking] = useState(false);
52
  const [isEditableModeEnabled, setIsEditableModeEnabled] = useState(false);
53
  const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
54
- null
55
  );
 
56
 
57
  /**
58
  * Resets the layout based on screen size
@@ -92,7 +94,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
92
  const editorWidth = e.clientX;
93
  const clampedEditorWidth = Math.max(
94
  minWidth,
95
- Math.min(editorWidth, maxWidth)
96
  );
97
  const calculatedPreviewWidth =
98
  window.innerWidth - clampedEditorWidth - resizerWidth;
@@ -121,7 +123,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
121
  onClick: () => {
122
  window.open(
123
  `https://huggingface.co/spaces/${project?.space_id}`,
124
- "_blank"
125
  );
126
  },
127
  },
@@ -169,6 +171,8 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
169
  preview.current.style.width = "100%";
170
  }
171
  }
 
 
172
  }, [currentTab]);
173
 
174
  const handleEditorValidation = (markers: editor.IMarker[]) => {
@@ -210,7 +214,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
210
  "h-full bg-neutral-900 transition-all duration-200 absolute left-0 top-0",
211
  {
212
  "pointer-events-none": isAiWorking,
213
- }
214
  )}
215
  options={{
216
  colorDecorators: true,
@@ -242,7 +246,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
242
  onSuccess={(
243
  finalHtml: string,
244
  p: string,
245
- updatedLines?: number[][]
246
  ) => {
247
  const currentHistory = [...htmlHistory];
248
  currentHistory.unshift({
@@ -262,7 +266,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
262
  line[0],
263
  1,
264
  line[1],
265
- 1
266
  ),
267
  options: {
268
  inlineClassName: "matched-line",
@@ -284,13 +288,14 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
284
  }}
285
  onScrollToBottom={() => {
286
  editorRef.current?.revealLine(
287
- editorRef.current?.getModel()?.getLineCount() ?? 0
288
  );
289
  }}
290
  isEditableModeEnabled={isEditableModeEnabled}
291
  setIsEditableModeEnabled={setIsEditableModeEnabled}
292
  selectedElement={selectedElement}
293
  setSelectedElement={setSelectedElement}
 
294
  />
295
  </div>
296
  <div
@@ -313,6 +318,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
313
  setSelectedElement(element);
314
  setCurrentTab("chat");
315
  }}
 
316
  />
317
  </main>
318
  <Footer
@@ -327,7 +333,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
327
  setHtml(defaultHTML);
328
  removeHtmlStorage();
329
  editorRef.current?.revealLine(
330
- editorRef.current?.getModel()?.getLineCount() ?? 0
331
  );
332
  }
333
  }}
 
26
  import { SaveButton } from "./save-button";
27
  import { LoadProject } from "../my-projects/load-project";
28
  import { isTheSameHtml } from "@/lib/compare-html-diff";
29
+ import type { PreviewError } from "@/types/preview-error";
30
 
31
  export const AppEditor = ({ project }: { project?: Project | null }) => {
32
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
 
52
  const [isAiWorking, setIsAiWorking] = useState(false);
53
  const [isEditableModeEnabled, setIsEditableModeEnabled] = useState(false);
54
  const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
55
+ null,
56
  );
57
+ const [previewErrors, setPreviewErrors] = useState<PreviewError[]>([]);
58
 
59
  /**
60
  * Resets the layout based on screen size
 
94
  const editorWidth = e.clientX;
95
  const clampedEditorWidth = Math.max(
96
  minWidth,
97
+ Math.min(editorWidth, maxWidth),
98
  );
99
  const calculatedPreviewWidth =
100
  window.innerWidth - clampedEditorWidth - resizerWidth;
 
123
  onClick: () => {
124
  window.open(
125
  `https://huggingface.co/spaces/${project?.space_id}`,
126
+ "_blank",
127
  );
128
  },
129
  },
 
171
  preview.current.style.width = "100%";
172
  }
173
  }
174
+ // Clear preview errors when switching tabs
175
+ setPreviewErrors([]);
176
  }, [currentTab]);
177
 
178
  const handleEditorValidation = (markers: editor.IMarker[]) => {
 
214
  "h-full bg-neutral-900 transition-all duration-200 absolute left-0 top-0",
215
  {
216
  "pointer-events-none": isAiWorking,
217
+ },
218
  )}
219
  options={{
220
  colorDecorators: true,
 
246
  onSuccess={(
247
  finalHtml: string,
248
  p: string,
249
+ updatedLines?: number[][],
250
  ) => {
251
  const currentHistory = [...htmlHistory];
252
  currentHistory.unshift({
 
266
  line[0],
267
  1,
268
  line[1],
269
+ 1,
270
  ),
271
  options: {
272
  inlineClassName: "matched-line",
 
288
  }}
289
  onScrollToBottom={() => {
290
  editorRef.current?.revealLine(
291
+ editorRef.current?.getModel()?.getLineCount() ?? 0,
292
  );
293
  }}
294
  isEditableModeEnabled={isEditableModeEnabled}
295
  setIsEditableModeEnabled={setIsEditableModeEnabled}
296
  selectedElement={selectedElement}
297
  setSelectedElement={setSelectedElement}
298
+ previewErrors={previewErrors}
299
  />
300
  </div>
301
  <div
 
318
  setSelectedElement(element);
319
  setCurrentTab("chat");
320
  }}
321
+ onErrors={setPreviewErrors}
322
  />
323
  </main>
324
  <Footer
 
333
  setHtml(defaultHTML);
334
  removeHtmlStorage();
335
  editorRef.current?.revealLine(
336
+ editorRef.current?.getModel()?.getLineCount() ?? 0,
337
  );
338
  }
339
  }}
components/editor/preview/index.tsx CHANGED
@@ -1,12 +1,15 @@
1
  "use client";
2
  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,
@@ -18,6 +21,7 @@ export const Preview = ({
18
  iframeRef,
19
  isEditableModeEnabled,
20
  onClickElement,
 
21
  }: {
22
  html: string;
23
  isResizing: boolean;
@@ -28,11 +32,29 @@ export const Preview = ({
28
  currentTab: string;
29
  isEditableModeEnabled?: boolean;
30
  onClickElement?: (element: HTMLElement) => void;
 
31
  }) => {
32
  const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
33
- null
34
  );
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  // add event listener to the iframe to track hovered elements
37
  const handleMouseOver = (event: MouseEvent) => {
38
  if (iframeRef?.current) {
@@ -100,6 +122,36 @@ export const Preview = ({
100
  return hoveredElement;
101
  }, [hoveredElement, isEditableModeEnabled]);
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  return (
104
  <div
105
  ref={ref}
@@ -109,7 +161,7 @@ export const Preview = ({
109
  "lg:p-4": currentTab !== "preview",
110
  "max-lg:h-0": currentTab === "chat",
111
  "max-lg:h-full": currentTab === "preview",
112
- }
113
  )}
114
  onClick={(e) => {
115
  if (isAiWorking) {
@@ -124,7 +176,7 @@ export const Preview = ({
124
  y={-1}
125
  strokeDasharray={"4 2"}
126
  className={cn(
127
- "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]"
128
  )}
129
  />
130
  {!isAiWorking && hoveredElement && selectedElement && (
@@ -158,10 +210,13 @@ export const Preview = ({
158
  device === "mobile",
159
  "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
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({
167
  block: isAiWorking ? "end" : "start",
 
1
  "use client";
2
  import { useUpdateEffect } from "react-use";
3
+ import { useMemo, useState, useEffect } 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
+ import { errorDetectorScript, ERROR_DETECTOR_ID } from "@/lib/error-detector";
11
+
12
+ import type { PreviewError } from "@/types/preview-error";
13
 
14
  export const Preview = ({
15
  html,
 
21
  iframeRef,
22
  isEditableModeEnabled,
23
  onClickElement,
24
+ onErrors,
25
  }: {
26
  html: string;
27
  isResizing: boolean;
 
32
  currentTab: string;
33
  isEditableModeEnabled?: boolean;
34
  onClickElement?: (element: HTMLElement) => void;
35
+ onErrors?: (errors: PreviewError[]) => void;
36
  }) => {
37
  const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
38
+ null,
39
  );
40
 
41
+ // Listen for error messages from the iframe
42
+ useEffect(() => {
43
+ const handleMessage = (event: MessageEvent) => {
44
+ if (event.data?.type === "PREVIEW_ERRORS" && onErrors) {
45
+ onErrors(event.data.errors);
46
+ }
47
+ };
48
+
49
+ window.addEventListener("message", handleMessage);
50
+ return () => window.removeEventListener("message", handleMessage);
51
+ }, [onErrors]);
52
+
53
+ // Defensive reset: Clear errors when HTML changes
54
+ useUpdateEffect(() => {
55
+ onErrors?.([]);
56
+ }, [html]);
57
+
58
  // add event listener to the iframe to track hovered elements
59
  const handleMouseOver = (event: MouseEvent) => {
60
  if (iframeRef?.current) {
 
122
  return hoveredElement;
123
  }, [hoveredElement, isEditableModeEnabled]);
124
 
125
+ // Inject error detection script into the HTML
126
+ const htmlWithErrorDetection = useMemo(() => {
127
+ if (!html) return "";
128
+
129
+ // First, remove any existing error detector script to prevent duplicates
130
+ const cleanedHtml = html.replace(
131
+ new RegExp(
132
+ `<script[^>]*id="${ERROR_DETECTOR_ID}"[^>]*>[\\s\\S]*?</script>`,
133
+ "gi",
134
+ ),
135
+ "",
136
+ );
137
+
138
+ // Create the script tag with proper ID
139
+ const scriptTag = `<script id="${ERROR_DETECTOR_ID}">${errorDetectorScript}</script>`;
140
+
141
+ // If html already has a </head> tag, inject script before it
142
+ if (cleanedHtml.includes("</head>")) {
143
+ return cleanedHtml.replace("</head>", `${scriptTag}</head>`);
144
+ }
145
+ // If html already has a <body> tag, inject script after it
146
+ else if (cleanedHtml.includes("<body")) {
147
+ return cleanedHtml.replace(/<body([^>]*)>/, `<body$1>${scriptTag}`);
148
+ }
149
+ // Otherwise, wrap the content with proper HTML structure
150
+ else {
151
+ return `<!DOCTYPE html><html><head>${scriptTag}</head><body>${cleanedHtml}</body></html>`;
152
+ }
153
+ }, [html]);
154
+
155
  return (
156
  <div
157
  ref={ref}
 
161
  "lg:p-4": currentTab !== "preview",
162
  "max-lg:h-0": currentTab === "chat",
163
  "max-lg:h-full": currentTab === "preview",
164
+ },
165
  )}
166
  onClick={(e) => {
167
  if (isAiWorking) {
 
176
  y={-1}
177
  strokeDasharray={"4 2"}
178
  className={cn(
179
+ "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]",
180
  )}
181
  />
182
  {!isAiWorking && hoveredElement && selectedElement && (
 
210
  device === "mobile",
211
  "lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:rounded-[24px]":
212
  currentTab !== "preview" && device === "desktop",
213
+ },
214
  )}
215
+ srcDoc={htmlWithErrorDetection}
216
  onLoad={() => {
217
+ // Clear errors on fresh load as extra safety
218
+ onErrors?.([]);
219
+
220
  if (iframeRef?.current?.contentWindow?.document?.body) {
221
  iframeRef.current.contentWindow.document.body.scrollIntoView({
222
  block: isAiWorking ? "end" : "start",
lib/constants.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ // Auto-fix feature constants
2
+ export const MAX_AUTO_FIX_ATTEMPTS = 3;
3
+ export const AUTO_FIX_DELAY_MS = 2000;
4
+ export const ERROR_BATCH_DELAY_MS = 1000;
5
+ export const MAX_ERRORS_TO_DISPLAY = 10;
lib/error-detector.ts ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const ERROR_DETECTOR_ID = "deepsite-error-detector";
2
+
3
+ export const errorDetectorScript = `
4
+ (function() {
5
+ const errors = [];
6
+ let errorTimeout = null;
7
+ const MAX_ERRORS = 10;
8
+ const BATCH_DELAY = 1000; // Wait 1 second before sending errors
9
+
10
+ // Create a safe error object that can be serialized
11
+ function createSafeError(error, type, context = {}) {
12
+ return {
13
+ type,
14
+ message: error?.message || String(error),
15
+ stack: error?.stack,
16
+ lineNumber: error?.lineNumber || context.lineNumber,
17
+ columnNumber: error?.columnNumber || context.columnNumber,
18
+ fileName: error?.fileName || context.fileName,
19
+ timestamp: new Date().toISOString(),
20
+ ...context
21
+ };
22
+ }
23
+
24
+ // Send errors to parent window (always send, even if empty)
25
+ function sendErrors() {
26
+ window.parent.postMessage({
27
+ type: 'PREVIEW_ERRORS',
28
+ errors: errors.slice(0, MAX_ERRORS), // Limit errors sent
29
+ url: window.location.href
30
+ }, '*');
31
+
32
+ errors.length = 0; // Clear sent errors
33
+ }
34
+
35
+ // Batch errors to avoid spamming
36
+ function queueError(error) {
37
+ errors.push(error);
38
+
39
+ if (errorTimeout) clearTimeout(errorTimeout);
40
+ errorTimeout = setTimeout(sendErrors, BATCH_DELAY);
41
+ }
42
+
43
+ // Global error handler
44
+ window.addEventListener('error', function(event) {
45
+ const error = createSafeError(event.error || event.message, 'runtime-error', {
46
+ lineNumber: event.lineno,
47
+ columnNumber: event.colno,
48
+ fileName: event.filename,
49
+ errorType: 'JavaScript Error'
50
+ });
51
+ queueError(error);
52
+ });
53
+
54
+ // Unhandled promise rejection handler
55
+ window.addEventListener('unhandledrejection', function(event) {
56
+ const error = createSafeError(event.reason, 'unhandled-promise', {
57
+ promise: event.promise,
58
+ errorType: 'Unhandled Promise Rejection'
59
+ });
60
+ queueError(error);
61
+ });
62
+
63
+ // Override console.error to catch logged errors
64
+ const originalConsoleError = console.error;
65
+ console.error = function(...args) {
66
+ originalConsoleError.apply(console, args);
67
+
68
+ const error = createSafeError(args.join(' '), 'console-error', {
69
+ errorType: 'Console Error',
70
+ args: args.map(arg => String(arg))
71
+ });
72
+ queueError(error);
73
+ };
74
+
75
+ // Monitor failed resource loads (404s, etc)
76
+ window.addEventListener('error', function(event) {
77
+ if (event.target !== window) {
78
+ // This is a resource loading error
79
+ const target = event.target;
80
+ const error = createSafeError(\`Failed to load resource: \${target.src || target.href}\`, 'resource-error', {
81
+ tagName: target.tagName,
82
+ src: target.src || target.href,
83
+ errorType: 'Resource Loading Error'
84
+ });
85
+ queueError(error);
86
+ }
87
+ }, true); // Use capture phase to catch resource errors
88
+
89
+ // Monitor for common React errors
90
+ if (window.React && window.React.version) {
91
+ const originalError = console.error;
92
+ console.error = function(...args) {
93
+ originalError.apply(console, args);
94
+
95
+ const errorString = args.join(' ');
96
+ if (errorString.includes('ReactDOM.render is no longer supported') ||
97
+ errorString.includes('Cannot read property') ||
98
+ errorString.includes('Cannot access property')) {
99
+ const error = createSafeError(errorString, 'react-error', {
100
+ errorType: 'React Error',
101
+ reactVersion: window.React.version
102
+ });
103
+ queueError(error);
104
+ }
105
+ };
106
+ }
107
+
108
+ // Report current state of errors
109
+ function reportCurrentState() {
110
+ sendErrors();
111
+ }
112
+
113
+ // Send initial ready message and current error state
114
+ window.addEventListener('load', reportCurrentState);
115
+
116
+ // Monitor for DOM changes that might clear errors
117
+ const observer = new MutationObserver(() => {
118
+ // Small delay to let any new errors register
119
+ setTimeout(reportCurrentState, 100);
120
+ });
121
+
122
+ // Start observing once DOM is ready
123
+ if (document.body) {
124
+ observer.observe(document.body, {
125
+ subtree: true,
126
+ childList: true
127
+ });
128
+ } else {
129
+ window.addEventListener('DOMContentLoaded', () => {
130
+ observer.observe(document.body, {
131
+ subtree: true,
132
+ childList: true
133
+ });
134
+ });
135
+ }
136
+
137
+ // Send initial state
138
+ reportCurrentState();
139
+ })();
140
+ `;
lib/error-formatter.ts ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { PreviewError } from "@/types/preview-error";
2
+
3
+ // Sanitize error messages to prevent prompt injection
4
+ function sanitizeErrorMessage(message: string): string {
5
+ return message
6
+ .replace(/[<>]/g, "") // Remove potential HTML tags
7
+ .replace(/```/g, "'''") // Escape code blocks
8
+ .slice(0, 500); // Limit length
9
+ }
10
+
11
+ export function formatErrorsForAI(
12
+ errors: PreviewError[],
13
+ html: string
14
+ ): string {
15
+ if (!errors || errors.length === 0) return "";
16
+
17
+ // Validate errors array
18
+ const validErrors = errors.filter((e) => e && typeof e.message === "string");
19
+ if (validErrors.length === 0) return "";
20
+
21
+ // Group errors by type for better organization
22
+ const errorGroups = validErrors.reduce((acc, error) => {
23
+ const type = error.errorType || error.type;
24
+ if (!acc[type]) acc[type] = [];
25
+ acc[type].push(error);
26
+ return acc;
27
+ }, {} as Record<string, PreviewError[]>);
28
+
29
+ let formattedErrors =
30
+ "The following errors were detected in the preview:\n\n";
31
+
32
+ // Format each error group
33
+ Object.entries(errorGroups).forEach(([type, groupErrors]) => {
34
+ formattedErrors += `### ${type} (${groupErrors.length} error${
35
+ groupErrors.length > 1 ? "s" : ""
36
+ })\n\n`;
37
+
38
+ groupErrors.forEach((error, index) => {
39
+ const sanitizedMessage = sanitizeErrorMessage(error.message);
40
+ formattedErrors += `${index + 1}. **${sanitizedMessage}**\n`;
41
+
42
+ if (error.lineNumber) {
43
+ formattedErrors += ` - Line: ${error.lineNumber}`;
44
+ if (error.columnNumber) {
45
+ formattedErrors += `, Column: ${error.columnNumber}`;
46
+ }
47
+ formattedErrors += "\n";
48
+ }
49
+
50
+ if (error.fileName && error.fileName !== "undefined") {
51
+ formattedErrors += ` - File: ${error.fileName}\n`;
52
+ }
53
+
54
+ // For resource errors, include the problematic resource
55
+ if (error.type === "resource-error" && error.src) {
56
+ formattedErrors += ` - Resource: ${error.src}\n`;
57
+ formattedErrors += ` - Tag: <${error.tagName?.toLowerCase()}>\n`;
58
+ }
59
+
60
+ // Include relevant code snippet if we have line numbers
61
+ if (error.lineNumber && html) {
62
+ const lines = html.split("\n");
63
+ const startLine = Math.max(0, error.lineNumber - 3);
64
+ const endLine = Math.min(lines.length, error.lineNumber + 2);
65
+
66
+ if (lines[error.lineNumber - 1]) {
67
+ formattedErrors += " - Code context:\n";
68
+ formattedErrors += " ```html\n";
69
+ for (let i = startLine; i < endLine; i++) {
70
+ const marker = i === error.lineNumber - 1 ? ">" : " ";
71
+ formattedErrors += ` ${marker} ${i + 1}: ${lines[i]}\n`;
72
+ }
73
+ formattedErrors += " ```\n";
74
+ }
75
+ }
76
+
77
+ formattedErrors += "\n";
78
+ });
79
+ });
80
+
81
+ return formattedErrors;
82
+ }
83
+
84
+ export function createErrorFixPrompt(
85
+ errors: PreviewError[],
86
+ html: string
87
+ ): string {
88
+ const formattedErrors = formatErrorsForAI(errors, html);
89
+
90
+ return `${formattedErrors}
91
+ Please fix these errors in the HTML code. Focus on:
92
+ 1. Fixing JavaScript syntax errors
93
+ 2. Resolving undefined variables or functions
94
+ 3. Fixing broken resource links (404s)
95
+ 4. Ensuring all referenced libraries are properly loaded
96
+ 5. Fixing any HTML structure issues
97
+
98
+ Make the minimum necessary changes to fix the errors while preserving the intended functionality.`;
99
+ }
100
+
101
+ // Check if errors are likely fixable by AI
102
+ export function areErrorsFixable(errors: PreviewError[]): boolean {
103
+ if (!errors || errors.length === 0) return false;
104
+
105
+ // Filter out errors that are likely not fixable
106
+ const fixableErrors = errors.filter((error) => {
107
+ // Skip errors from external resources
108
+ if (
109
+ error.fileName &&
110
+ (error.fileName.includes("http://") ||
111
+ error.fileName.includes("https://"))
112
+ ) {
113
+ return false;
114
+ }
115
+
116
+ // Skip certain console errors that might be intentional
117
+ if (
118
+ error.type === "console-error" &&
119
+ error.message.includes("Development mode")
120
+ ) {
121
+ return false;
122
+ }
123
+
124
+ return true;
125
+ });
126
+
127
+ return fixableErrors.length > 0;
128
+ }
129
+
130
+ // Deduplicate similar errors
131
+ export function deduplicateErrors(errors: PreviewError[]): PreviewError[] {
132
+ const seen = new Set<string>();
133
+ return errors.filter((error) => {
134
+ const key = `${error.type}-${error.message}-${error.lineNumber || 0}`;
135
+ if (seen.has(key)) return false;
136
+ seen.add(key);
137
+ return true;
138
+ });
139
+ }
types/preview-error.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface PreviewError {
2
+ type:
3
+ | "runtime-error"
4
+ | "unhandled-promise"
5
+ | "console-error"
6
+ | "resource-error"
7
+ | "react-error";
8
+ message: string;
9
+ stack?: string;
10
+ lineNumber?: number;
11
+ columnNumber?: number;
12
+ fileName?: string;
13
+ timestamp: string;
14
+ errorType?: string;
15
+ // For resource errors
16
+ tagName?: string;
17
+ src?: string;
18
+ // For React errors
19
+ reactVersion?: string;
20
+ // For console errors
21
+ args?: string[];
22
+ }