From bb74b69287ea31301256bb712817040c9060476a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Casta=C3=B1o?= Date: Fri, 13 Jun 2025 12:33:55 -0700 Subject: [PATCH] Adding error screen --- app/api/generate/route.ts | 10 +- app/chat/page.tsx | 10 ++ components/ui-builder/error-display.tsx | 117 ++++++++++++++++++++++ components/ui-builder/preview-card.tsx | 27 ++++- components/ui/badge.tsx | 46 +++++++++ lib/e2b.ts | 127 +++++++++++++++++++++++- package-lock.json | 62 ++++++++---- package.json | 2 +- 8 files changed, 368 insertions(+), 33 deletions(-) create mode 100644 components/ui-builder/error-display.tsx create mode 100644 components/ui/badge.tsx diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts index ba509b0..38f6dc2 100644 --- a/app/api/generate/route.ts +++ b/app/api/generate/route.ts @@ -52,14 +52,16 @@ export async function POST(request: NextRequest) { // } // } - const { sbxId, template, url, allFiles } = await createSandbox({ files: repairedFiles }); + const sandboxResult = await createSandbox({ files: repairedFiles }); // Return the results to the client return NextResponse.json({ originalFiles: generatedFiles, - repairedFiles: allFiles, // Use the allFiles from the sandbox - buildOutput: `Sandbox created with template: ${template}, ID: ${sbxId}`, - previewUrl: url, + repairedFiles: sandboxResult.allFiles, // Use the allFiles from the sandbox + buildOutput: `Sandbox created with template: ${sandboxResult.template}, ID: ${sandboxResult.sbxId}`, + previewUrl: sandboxResult.url, + buildErrors: sandboxResult.buildErrors, + hasErrors: sandboxResult.hasErrors, }); } catch (error) { console.error('Error generating app:', error); diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 97b1075..7fcd76e 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -12,6 +12,14 @@ type GenerationResult = { originalFiles?: z.infer; buildOutput: string; previewUrl: string; + buildErrors?: Array<{ + type: 'typescript' | 'build' | 'runtime'; + message: string; + file?: string; + line?: number; + column?: number; + }>; + hasErrors?: boolean; }; export default function ChatPage() { @@ -103,6 +111,8 @@ export default function ChatPage() { code={result?.repairedFiles || result?.originalFiles || []} isGenerating={isGenerating} prompt={initialPrompt} + buildErrors={result?.buildErrors} + hasErrors={result?.hasErrors} /> diff --git a/components/ui-builder/error-display.tsx b/components/ui-builder/error-display.tsx new file mode 100644 index 0000000..9e86626 --- /dev/null +++ b/components/ui-builder/error-display.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { AlertCircle, Code, FileX, Terminal } from 'lucide-react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +interface BuildError { + type: 'typescript' | 'build' | 'runtime'; + message: string; + file?: string; + line?: number; + column?: number; +} + +interface ErrorDisplayProps { + errors: BuildError[]; +} + +export function ErrorDisplay({ errors }: ErrorDisplayProps) { + const getErrorIcon = (type: BuildError['type']) => { + switch (type) { + case 'typescript': + return ; + case 'build': + return ; + case 'runtime': + return ; + default: + return ; + } + }; + + const getErrorColor = (type: BuildError['type']) => { + switch (type) { + case 'typescript': + return 'bg-blue-500/10 text-blue-700 dark:text-blue-400'; + case 'build': + return 'bg-orange-500/10 text-orange-700 dark:text-orange-400'; + case 'runtime': + return 'bg-red-500/10 text-red-700 dark:text-red-400'; + default: + return 'bg-gray-500/10 text-gray-700 dark:text-gray-400'; + } + }; + + const groupedErrors = errors.reduce((acc, error) => { + if (!acc[error.type]) { + acc[error.type] = []; + } + acc[error.type].push(error); + return acc; + }, {} as Record); + + return ( +
+
+
+ +

Build Errors Detected

+

+ Your project has some issues that need to be fixed before it can run properly. +

+
+ + +
+ {Object.entries(groupedErrors).map(([type, typeErrors]) => ( +
+
+ {getErrorIcon(type as BuildError['type'])} +

{type} Errors

+ + {typeErrors.length} + +
+ + {typeErrors.map((error, index) => ( + + + + {error.type} + + {error.file && ( + + {error.file} + {error.line && `:${error.line}`} + {error.column && `:${error.column}`} + + )} + + + {error.message} + + + ))} +
+ ))} +
+
+ +
+

💡 Common Solutions:

+
    +
  • • Check for missing imports or typos in component names
  • +
  • • Verify that all props are properly typed
  • +
  • • Make sure all dependencies are correctly installed
  • +
  • • Try regenerating the component with more specific requirements
  • +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/ui-builder/preview-card.tsx b/components/ui-builder/preview-card.tsx index dc58e32..01e4196 100644 --- a/components/ui-builder/preview-card.tsx +++ b/components/ui-builder/preview-card.tsx @@ -6,6 +6,7 @@ import { benchifyFileSchema } from "@/lib/schemas"; import { z } from "zod"; import { CodeEditor } from "./code-editor"; import { DownloadButton } from "./download-button"; +import { ErrorDisplay } from "./error-display"; interface Step { id: string; @@ -36,14 +37,31 @@ const GENERATION_STEPS: Step[] = [ }, ]; +interface BuildError { + type: 'typescript' | 'build' | 'runtime'; + message: string; + file?: string; + line?: number; + column?: number; +} + interface PreviewCardProps { previewUrl?: string; code: z.infer; isGenerating?: boolean; prompt?: string; + buildErrors?: BuildError[]; + hasErrors?: boolean; } -export function PreviewCard({ previewUrl, code, isGenerating = false, prompt }: PreviewCardProps) { +export function PreviewCard({ + previewUrl, + code, + isGenerating = false, + prompt, + buildErrors = [], + hasErrors = false +}: PreviewCardProps) { const files = code || []; const [currentStep, setCurrentStep] = useState(0); @@ -111,8 +129,8 @@ export function PreviewCard({ previewUrl, code, isGenerating = false, prompt }:

{step.label}

@@ -127,6 +145,9 @@ export function PreviewCard({ previewUrl, code, isGenerating = false, prompt }:
+ ) : hasErrors && buildErrors.length > 0 ? ( + // Show build errors if there are any + ) : previewUrl ? ( // Show the actual preview iframe when ready
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/lib/e2b.ts b/lib/e2b.ts index c4d654c..b03988e 100644 --- a/lib/e2b.ts +++ b/lib/e2b.ts @@ -10,8 +10,24 @@ if (!E2B_API_KEY) { throw new Error('E2B_API_KEY is not set'); } +interface BuildError { + type: 'typescript' | 'build' | 'runtime'; + message: string; + file?: string; + line?: number; + column?: number; +} + +interface SandboxResult { + sbxId: string; + template: string; + url: string; + allFiles: z.infer; + buildErrors?: BuildError[]; + hasErrors: boolean; +} -export async function createSandbox({ files }: { files: z.infer }) { +export async function createSandbox({ files }: { files: z.infer }): Promise { // Create sandbox from the improved template const sandbox = await Sandbox.create('vite-support', { apiKey: E2B_API_KEY }); console.log(`Sandbox created: ${sandbox.sandboxId}`); @@ -27,6 +43,8 @@ export async function createSandbox({ files }: { files: z.infer file.path === 'package.json'); if (packageJsonFile) { @@ -41,14 +59,60 @@ export async function createSandbox({ files }: { files: z.infer 0 ? buildErrors : undefined, + hasErrors: buildErrors.length > 0 }; } +function parseTypeScriptErrors(stderr: string): BuildError[] { + const errors: BuildError[] = []; + const lines = stderr.split('\n'); + + for (const line of lines) { + // Match TypeScript error pattern: file(line,column): error TS####: message + const match = line.match(/(.+)\((\d+),(\d+)\): error TS\d+: (.+)/); + if (match) { + const [, file, line, column, message] = match; + errors.push({ + type: 'typescript', + message: message.trim(), + file: file.replace('/app/', ''), + line: parseInt(line), + column: parseInt(column) + }); + } + } + + // If no specific errors found but stderr has content, add generic error + if (errors.length === 0 && stderr.trim()) { + errors.push({ + type: 'typescript', + message: 'TypeScript compilation failed: ' + stderr.trim() + }); + } + + return errors; +} + +function parseViteBuildErrors(stderr: string): BuildError[] { + const errors: BuildError[] = []; + const lines = stderr.split('\n'); + + for (const line of lines) { + // Match Vite build error patterns + if (line.includes('error') || line.includes('Error')) { + errors.push({ + type: 'build', + message: line.trim() + }); + } + } + + // If no specific errors found but stderr has content, add generic error + if (errors.length === 0 && stderr.trim()) { + errors.push({ + type: 'build', + message: 'Build failed: ' + stderr.trim() + }); + } + + return errors; +} + function extractNewPackages(packageJsonContent: string): string[] { try { const packageJson = JSON.parse(packageJsonContent); diff --git a/package-lock.json b/package-lock.json index 9bade0a..293347c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-scroll-area": "^1.2.8", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", "@types/react-syntax-highlighter": "^15.5.13", @@ -1226,24 +1226,6 @@ } } }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", @@ -1270,6 +1252,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1403,6 +1403,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", @@ -1466,9 +1484,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" diff --git a/package.json b/package.json index 4c41102..4a5d160 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-scroll-area": "^1.2.8", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", "@types/react-syntax-highlighter": "^15.5.13",