diff --git a/e2e-tests/fix_error.spec.ts b/e2e-tests/fix_error.spec.ts index 08f7202..0155997 100644 --- a/e2e-tests/fix_error.spec.ts +++ b/e2e-tests/fix_error.spec.ts @@ -1,4 +1,4 @@ -import { testSkipIfWindows } from "./helpers/test_helper"; +import { testSkipIfWindows, test } from "./helpers/test_helper"; testSkipIfWindows("fix error with AI", async ({ po }) => { await po.setUp({ autoApprove: true }); @@ -19,3 +19,13 @@ testSkipIfWindows("fix error with AI", async ({ po }) => { // await po.locatePreviewErrorBanner().waitFor({ state: "hidden" }); await po.snapshotPreview(); }); + +test("fix all errors button", async ({ po }) => { + await po.setUp({ autoApprove: true }); + await po.sendPrompt("tc=create-multiple-errors"); + + await po.clickFixAllErrors(); + await po.waitForChatCompletion(); + + await po.snapshotMessages(); +}); diff --git a/e2e-tests/fixtures/create-multiple-errors.md b/e2e-tests/fixtures/create-multiple-errors.md new file mode 100644 index 0000000..9b5df55 --- /dev/null +++ b/e2e-tests/fixtures/create-multiple-errors.md @@ -0,0 +1,55 @@ +I will intentionally add multiple errors to test the Fix All Errors button + + +// Update this page (the content is just a fallback if you fail to update the page) + +import { MadeWithDyad } from "@/components/made-with-dyad"; + +const Index = () => { +throw new Error("First error in Index"); +return ( + + + +Welcome to Your Blank App + +Start building your amazing project here! + + + + +); +}; + +export default Index; + + + +Error: First error in Index + at Index (http://localhost:5173/src/pages/Index.tsx:6:7) + + + +const ErrorComponent = () => { + throw new Error("Second error in ErrorComponent"); + return This will never render; +}; + +export default ErrorComponent; + + + +Error: Second error in ErrorComponent + at ErrorComponent (http://localhost:5173/src/components/ErrorComponent.tsx:2:9) + + + +export const brokenHelper = () => { + throw new Error("Third error in helper"); +}; + + + +Error: Third error in helper + at brokenHelper (http://localhost:5173/src/utils/helper.ts:2:9) + \ No newline at end of file diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index 8d35fd8..d9ab5a5 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -575,6 +575,10 @@ export class PageObject { await this.page.getByRole("button", { name: "Fix error with AI" }).click(); } + async clickFixAllErrors() { + await this.page.getByRole("button", { name: /Fix All Errors/ }).click(); + } + async snapshotPreviewErrorBanner() { await expect(this.locatePreviewErrorBanner()).toMatchAriaSnapshot({ timeout: Timeout.LONG, diff --git a/e2e-tests/snapshots/fix_error.spec.ts_fix-all-errors-button-1.aria.yml b/e2e-tests/snapshots/fix_error.spec.ts_fix-all-errors-button-1.aria.yml new file mode 100644 index 0000000..d202b96 --- /dev/null +++ b/e2e-tests/snapshots/fix_error.spec.ts_fix-all-errors-button-1.aria.yml @@ -0,0 +1,72 @@ +- paragraph: tc=create-multiple-errors +- paragraph: I will intentionally add multiple errors to test the Fix All Errors button +- img +- text: Index.tsx +- button "Edit": + - img +- img +- text: "src/pages/Index.tsx Summary: intentionally add first error" +- img +- text: Error +- button "Fix with AI": + - img +- text: First error in Index... +- img +- img +- text: ErrorComponent.tsx +- button "Edit": + - img +- img +- text: "src/components/ErrorComponent.tsx Summary: intentionally add second error" +- img +- text: Error +- button "Fix with AI": + - img +- text: Second error in ErrorComponent... +- img +- img +- text: helper.ts +- button "Edit": + - img +- img +- text: "src/utils/helper.ts Summary: intentionally add third error" +- img +- text: Error +- button "Fix with AI": + - img +- text: Third error in helper... +- img +- button "Fix All Errors (3)": + - img +- button: + - img +- img +- text: Approved +- img +- text: less than a minute ago +- img +- text: wrote 3 file(s) +- paragraph: "Fix all of the following errors:" +- list: + - listitem: First error in Index + - listitem: Second error in ErrorComponent + - listitem: Third error in helper +- img +- text: file1.txt +- button "Edit": + - img +- img +- text: file1.txt +- paragraph: More EOM +- button: + - img +- img +- text: Approved +- img +- text: less than a minute ago +- img +- text: wrote 1 file(s) +- button "Undo": + - img +- button "Retry": + - img \ No newline at end of file diff --git a/src/components/chat/DyadMarkdownParser.tsx b/src/components/chat/DyadMarkdownParser.tsx index f1ac697..ad8fddb 100644 --- a/src/components/chat/DyadMarkdownParser.tsx +++ b/src/components/chat/DyadMarkdownParser.tsx @@ -28,6 +28,7 @@ import { DyadCodeSearch } from "./DyadCodeSearch"; import { DyadRead } from "./DyadRead"; import { mapActionToButton } from "./ChatInput"; import { SuggestedAction } from "@/lib/schemas"; +import { FixAllErrorsButton } from "./FixAllErrorsButton"; interface DyadMarkdownParserProps { content: string; @@ -90,6 +91,34 @@ export const DyadMarkdownParser: React.FC = ({ return parseCustomTags(content); }, [content]); + // Extract error messages and track positions + const { errorMessages, lastErrorIndex, errorCount } = useMemo(() => { + const errors: string[] = []; + let lastIndex = -1; + let count = 0; + + contentPieces.forEach((piece, index) => { + if ( + piece.type === "custom-tag" && + piece.tagInfo.tag === "dyad-output" && + piece.tagInfo.attributes.type === "error" + ) { + const errorMessage = piece.tagInfo.attributes.message; + if (errorMessage?.trim()) { + errors.push(errorMessage.trim()); + count++; + lastIndex = index; + } + } + }); + + return { + errorMessages: errors, + lastErrorIndex: lastIndex, + errorCount: count, + }; + }, [contentPieces]); + return ( <> {contentPieces.map((piece, index) => ( @@ -106,6 +135,17 @@ export const DyadMarkdownParser: React.FC = ({ ) : renderCustomTag(piece.tagInfo, { isStreaming })} + {index === lastErrorIndex && + errorCount > 1 && + !isStreaming && + chatId && ( + + + + )} ))} > diff --git a/src/components/chat/FixAllErrorsButton.tsx b/src/components/chat/FixAllErrorsButton.tsx new file mode 100644 index 0000000..499f4e2 --- /dev/null +++ b/src/components/chat/FixAllErrorsButton.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/button"; +import { useStreamChat } from "@/hooks/useStreamChat"; +import { Sparkles, Loader2 } from "lucide-react"; +import { useState } from "react"; + +interface FixAllErrorsButtonProps { + errorMessages: string[]; + chatId: number; +} + +export function FixAllErrorsButton({ + errorMessages, + chatId, +}: FixAllErrorsButtonProps) { + const { streamMessage } = useStreamChat(); + const [isLoading, setIsLoading] = useState(false); + + const handleFixAllErrors = () => { + setIsLoading(true); + const allErrors = errorMessages + .map((msg, i) => `${i + 1}. ${msg}`) + .join("\n"); + + streamMessage({ + prompt: `Fix all of the following errors:\n\n${allErrors}`, + chatId, + onSettled: () => setIsLoading(false), + }); + }; + + return ( + + {isLoading ? ( + + ) : ( + + )} + Fix All Errors ({errorMessages.length}) + + ); +}
+Start building your amazing project here! +