Spaces:
Running
Running
errors-forwardings
#290
by
victor
HF Staff
- opened
- 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 |
+
}
|