Spaces:
Running
Running
feat: add automatic error detection and fixing for preview
Browse files- Add error detection script that captures runtime, syntax, and resource errors in iframe
- Implement auto-fix UI with toggle button and attempt counter (max 3 attempts)
- Create error formatter to convert errors into AI-friendly prompts
- Add TypeScript types for preview errors
- Implement defensive resets to prevent stale error state
- Add proper error sanitization and logging controls
- Extract magic numbers to constants
- Fix infinite loop issue by always reporting error state
The feature detects JavaScript errors in the preview iframe and automatically
sends them to the AI for correction, with safety limits and user controls.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app/api/ask-ai/route.ts +24 -3
- components/editor/ask-ai/index.tsx +130 -12
- components/editor/index.tsx +14 -8
- components/editor/preview/index.tsx +61 -6
- lib/constants.ts +5 -0
- lib/error-detector.ts +140 -0
- lib/error-formatter.ts +139 -0
- types/preview-error.ts +22 -0
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 {
|
227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
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())
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
374 |
-
|
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={
|
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 |
+
}
|