adding a button for fixing all errors (#1785)
closes #1688 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add a “Fix All Errors” button to the chat that collects all error messages and sends a single request to resolve them. This helps users fix multiple errors in one step. - New Features - Parse dyad-output type=error messages and track count/last index in DyadMarkdownParser. - Show FixAllErrorsButton after the last error when there are 2+ errors, not streaming, and chatId is present. - Button streams a prompt listing all errors, shows a loading state, and displays the error count. <sup>Written for commit b9762955d3b9cecd3b00c9efb478ce599f60e32d. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
committed by
GitHub
parent
40aeed1456
commit
90c5805b57
@@ -1,4 +1,4 @@
|
|||||||
import { testSkipIfWindows } from "./helpers/test_helper";
|
import { testSkipIfWindows, test } from "./helpers/test_helper";
|
||||||
|
|
||||||
testSkipIfWindows("fix error with AI", async ({ po }) => {
|
testSkipIfWindows("fix error with AI", async ({ po }) => {
|
||||||
await po.setUp({ autoApprove: true });
|
await po.setUp({ autoApprove: true });
|
||||||
@@ -19,3 +19,13 @@ testSkipIfWindows("fix error with AI", async ({ po }) => {
|
|||||||
// await po.locatePreviewErrorBanner().waitFor({ state: "hidden" });
|
// await po.locatePreviewErrorBanner().waitFor({ state: "hidden" });
|
||||||
await po.snapshotPreview();
|
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();
|
||||||
|
});
|
||||||
|
|||||||
55
e2e-tests/fixtures/create-multiple-errors.md
Normal file
55
e2e-tests/fixtures/create-multiple-errors.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
I will intentionally add multiple errors to test the Fix All Errors button
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Index.tsx" description="intentionally add first error">
|
||||||
|
// 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 (
|
||||||
|
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
|
||||||
|
<p className="text-xl text-gray-600">
|
||||||
|
Start building your amazing project here!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<MadeWithDyad />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-output type="error" message="First error in Index">
|
||||||
|
Error: First error in Index
|
||||||
|
at Index (http://localhost:5173/src/pages/Index.tsx:6:7)
|
||||||
|
</dyad-output>
|
||||||
|
|
||||||
|
<dyad-write path="src/components/ErrorComponent.tsx" description="intentionally add second error">
|
||||||
|
const ErrorComponent = () => {
|
||||||
|
throw new Error("Second error in ErrorComponent");
|
||||||
|
return <div>This will never render</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorComponent;
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-output type="error" message="Second error in ErrorComponent">
|
||||||
|
Error: Second error in ErrorComponent
|
||||||
|
at ErrorComponent (http://localhost:5173/src/components/ErrorComponent.tsx:2:9)
|
||||||
|
</dyad-output>
|
||||||
|
|
||||||
|
<dyad-write path="src/utils/helper.ts" description="intentionally add third error">
|
||||||
|
export const brokenHelper = () => {
|
||||||
|
throw new Error("Third error in helper");
|
||||||
|
};
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
<dyad-output type="error" message="Third error in helper">
|
||||||
|
Error: Third error in helper
|
||||||
|
at brokenHelper (http://localhost:5173/src/utils/helper.ts:2:9)
|
||||||
|
</dyad-output>
|
||||||
@@ -575,6 +575,10 @@ export class PageObject {
|
|||||||
await this.page.getByRole("button", { name: "Fix error with AI" }).click();
|
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() {
|
async snapshotPreviewErrorBanner() {
|
||||||
await expect(this.locatePreviewErrorBanner()).toMatchAriaSnapshot({
|
await expect(this.locatePreviewErrorBanner()).toMatchAriaSnapshot({
|
||||||
timeout: Timeout.LONG,
|
timeout: Timeout.LONG,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -28,6 +28,7 @@ import { DyadCodeSearch } from "./DyadCodeSearch";
|
|||||||
import { DyadRead } from "./DyadRead";
|
import { DyadRead } from "./DyadRead";
|
||||||
import { mapActionToButton } from "./ChatInput";
|
import { mapActionToButton } from "./ChatInput";
|
||||||
import { SuggestedAction } from "@/lib/schemas";
|
import { SuggestedAction } from "@/lib/schemas";
|
||||||
|
import { FixAllErrorsButton } from "./FixAllErrorsButton";
|
||||||
|
|
||||||
interface DyadMarkdownParserProps {
|
interface DyadMarkdownParserProps {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -90,6 +91,34 @@ export const DyadMarkdownParser: React.FC<DyadMarkdownParserProps> = ({
|
|||||||
return parseCustomTags(content);
|
return parseCustomTags(content);
|
||||||
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{contentPieces.map((piece, index) => (
|
{contentPieces.map((piece, index) => (
|
||||||
@@ -106,6 +135,17 @@ export const DyadMarkdownParser: React.FC<DyadMarkdownParserProps> = ({
|
|||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
)
|
)
|
||||||
: renderCustomTag(piece.tagInfo, { isStreaming })}
|
: renderCustomTag(piece.tagInfo, { isStreaming })}
|
||||||
|
{index === lastErrorIndex &&
|
||||||
|
errorCount > 1 &&
|
||||||
|
!isStreaming &&
|
||||||
|
chatId && (
|
||||||
|
<div className="mt-3 w-full flex">
|
||||||
|
<FixAllErrorsButton
|
||||||
|
errorMessages={errorMessages}
|
||||||
|
chatId={chatId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
47
src/components/chat/FixAllErrorsButton.tsx
Normal file
47
src/components/chat/FixAllErrorsButton.tsx
Normal file
@@ -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 (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={handleFixAllErrors}
|
||||||
|
className="bg-red-50 hover:bg-red-100 dark:bg-red-950 dark:hover:bg-red-900 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800 ml-auto hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 size={16} className="mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Sparkles size={16} className="mr-1" />
|
||||||
|
)}
|
||||||
|
Fix All Errors ({errorMessages.length})
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user