diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..3c22ef1
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,8 @@
+# OpenAI API Key for component generation
+OPENAI_API_KEY=sk-xxx
+
+# Benchify API Key for code repair
+BENCHIFY_API_KEY=bnch_xxx
+
+# E2B API Key for component preview
+E2B_API_KEY=e2b_xxx
diff --git a/.gitignore b/.gitignore
index 061a04a..f957c55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,7 @@ node_modules/
.next/
dist/
build/
+
+# Environment Variables
+.env
+.env*.local
diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts
new file mode 100644
index 0000000..3eb0089
--- /dev/null
+++ b/app/api/generate/route.ts
@@ -0,0 +1,64 @@
+// app/api/generate/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { generateApp } from '@/lib/openai';
+import { repairCode } from '@/lib/benchify';
+import { createSandbox, prepareVueEnvironment, deployApp } from '@/lib/e2b';
+import { componentSchema } from '@/lib/schemas';
+import { benchifyFileSchema } from '@/lib/schemas';
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+
+ // Validate the request using Zod schema
+ const validationResult = componentSchema.safeParse(body);
+
+ if (!validationResult.success) {
+ return NextResponse.json(
+ { error: 'Invalid request format', details: validationResult.error.format() },
+ { status: 400 }
+ );
+ }
+
+ const { description } = validationResult.data;
+
+ // Generate the Vue app using OpenAI
+ const generatedFiles = await generateApp(description);
+
+ // // Parse through schema before passing to repair
+ // const validatedFiles = benchifyFileSchema.parse(generatedFiles);
+
+ // // Repair the generated code using Benchify's API
+ // const repairedFiles = await repairCode(validatedFiles);
+
+ // // Set up E2B sandbox for preview
+ let previewUrl = undefined;
+ try {
+ const sandbox = await createSandbox();
+ await prepareVueEnvironment(sandbox);
+ const { previewUrl: url } = await deployApp(sandbox, generatedFiles);
+ previewUrl = url;
+ } catch (error) {
+ console.error('Error setting up preview:', error);
+ }
+
+ console.log("Preview URL: ", previewUrl);
+
+ // Return the results to the client
+ return NextResponse.json({
+ originalFiles: generatedFiles,
+ // repairedFiles: repairedFiles,
+ buildOutput: '', // We don't get build output from Benchify in our current setup
+ previewUrl,
+ });
+ } catch (error) {
+ console.error('Error generating app:', error);
+ return NextResponse.json(
+ {
+ error: 'Failed to generate app',
+ message: error instanceof Error ? error.message : String(error)
+ },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index a2dc41e..97afb5e 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,26 +1,122 @@
@import "tailwindcss";
+@import "tw-animate-css";
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
+@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.141 0.005 285.823);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.141 0.005 285.823);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.141 0.005 285.823);
+ --primary: oklch(0.21 0.006 285.885);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.967 0.001 286.375);
+ --secondary-foreground: oklch(0.21 0.006 285.885);
+ --muted: oklch(0.967 0.001 286.375);
+ --muted-foreground: oklch(0.552 0.016 285.938);
+ --accent: oklch(0.967 0.001 286.375);
+ --accent-foreground: oklch(0.21 0.006 285.885);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.92 0.004 286.32);
+ --input: oklch(0.92 0.004 286.32);
+ --ring: oklch(0.705 0.015 286.067);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
+ --sidebar-primary: oklch(0.21 0.006 285.885);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.967 0.001 286.375);
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
+ --sidebar-border: oklch(0.92 0.004 286.32);
+ --sidebar-ring: oklch(0.705 0.015 286.067);
}
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+.dark {
+ --background: oklch(0.141 0.005 285.823);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.21 0.006 285.885);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.21 0.006 285.885);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.92 0.004 286.32);
+ --primary-foreground: oklch(0.21 0.006 285.885);
+ --secondary: oklch(0.274 0.006 286.033);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.274 0.006 286.033);
+ --muted-foreground: oklch(0.705 0.015 286.067);
+ --accent: oklch(0.274 0.006 286.033);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.552 0.016 285.938);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.21 0.006 285.885);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.274 0.006 286.033);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.552 0.016 285.938);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
}
diff --git a/app/page.tsx b/app/page.tsx
index 88f0cc9..eaa67a5 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,103 +1,28 @@
-import Image from "next/image";
+// app/page.tsx
+'use client';
+
+import { useState } from 'react';
+import { PromptForm } from '@/components/ui-builder/prompt-form';
+import { Card, CardContent } from '@/components/ui/card';
export default function Home() {
- return (
-
-
-
-
- -
- Get started by editing{" "}
-
- app/page.tsx
-
- .
-
- -
- Save and see your changes instantly.
-
-
+ const [result, setResult] = useState(null);
-
-
-
-
+ return (
+
+
+
+ UI App Builder
+
+
+ Generate UI components with AI and automatically repair issues with Benchify
+
+
+
+
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..5a3c750
--- /dev/null
+++ b/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "zinc",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/components/ui-builder/prompt-form.tsx b/components/ui-builder/prompt-form.tsx
new file mode 100644
index 0000000..82910ed
--- /dev/null
+++ b/components/ui-builder/prompt-form.tsx
@@ -0,0 +1,105 @@
+// components/ui-builder/prompt-form.tsx
+'use client';
+
+import { useState } from 'react';
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import * as z from "zod"
+import { Loader2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Textarea } from '@/components/ui/textarea';
+
+const formSchema = z.object({
+ description: z.string().min(10, {
+ message: "Description must be at least 10 characters.",
+ }),
+})
+
+export function PromptForm({
+ onGenerate
+}: {
+ onGenerate: (result: any) => void
+}) {
+ const [loading, setLoading] = useState(false);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ description: "",
+ },
+ })
+
+ async function onSubmit(values: z.infer) {
+ setLoading(true);
+ try {
+ const response = await fetch('/api/generate', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ type: 'component',
+ description: values.description,
+ preview: true,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to generate component');
+ }
+
+ const result = await response.json();
+ onGenerate(result);
+ } catch (error) {
+ console.error('Error generating component:', error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000..1421354
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,59 @@
+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 buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..d05bbc6
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/components/ui/form.tsx b/components/ui/form.tsx
new file mode 100644
index 0000000..524b986
--- /dev/null
+++ b/components/ui/form.tsx
@@ -0,0 +1,167 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ FormProvider,
+ useFormContext,
+ useFormState,
+ type ControllerProps,
+ type FieldPath,
+ type FieldValues,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState } = useFormContext()
+ const formState = useFormState({ name: fieldContext.name })
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<"div">) {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+}
+
+function FormLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormControl({ ...props }: React.ComponentProps) {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message ?? "") : props.children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+}
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..03295ca
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
new file mode 100644
index 0000000..fb5fbc3
--- /dev/null
+++ b/components/ui/label.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 0000000..6a2b524
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitive from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+function Switch({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Switch }
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000..497ba5e
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..7f21b5e
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/lib/benchify.ts b/lib/benchify.ts
new file mode 100644
index 0000000..cccac93
--- /dev/null
+++ b/lib/benchify.ts
@@ -0,0 +1,97 @@
+// lib/benchify.ts
+import {
+ benchifyRequestSchema,
+ benchifyFileSchema,
+ type BenchifyFixerResponse
+} from './schemas';
+import { applyPatch } from 'diff';
+import { z } from 'zod';
+
+const BENCHIFY_API_KEY = process.env.BENCHIFY_API_KEY;
+const BENCHIFY_API_URL = 'https://api.benchify.com/v1';
+
+if (!BENCHIFY_API_KEY) {
+ throw new Error('BENCHIFY_API_KEY is not set');
+}
+
+// Repair code using Benchify Fixer API
+export async function repairCode(files: z.infer): Promise> {
+ try {
+ // Simple build command to verify syntax
+ const buildCmd = "npm run dev";
+
+ // Validate request against Benchify API schema
+ const requestData = benchifyRequestSchema.parse({
+ files,
+ jobName: "ui-component-fix",
+ buildCmd
+ });
+
+ console.log('Sending request to Benchify:', {
+ url: `${BENCHIFY_API_URL}/v1/fixer`,
+ filesCount: files.length,
+ jobName: requestData.jobName
+ });
+
+ // Send request to Benchify API
+ const response = await fetch(`${BENCHIFY_API_URL}/v1/fixer`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${BENCHIFY_API_KEY}`,
+ },
+ body: JSON.stringify(requestData),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => null);
+ console.error('Benchify API Error:', {
+ status: response.status,
+ statusText: response.statusText,
+ headers: Object.fromEntries(response.headers.entries()),
+ errorData,
+ requestData: {
+ filesCount: files.length,
+ jobName: requestData.jobName,
+ buildCmd: requestData.buildCmd
+ }
+ });
+ throw new Error(`Benchify API error: ${response.statusText}`);
+ }
+
+ // Parse the response
+ const result = await response.json() as BenchifyFixerResponse;
+ console.log('Benchify API Response:', {
+ buildStatus: result.build_status,
+ hasDiff: !!result.diff,
+ buildOutputLength: result.build_output?.length || 0
+ });
+
+ // Parse and apply patches to each file if diff exists
+ const parsedFiles = benchifyFileSchema.parse(files);
+ const repairedFiles = parsedFiles.map(file => {
+ if (result.diff) {
+ const patchResult = applyPatch(file.contents, result.diff);
+ return {
+ path: file.path,
+ contents: typeof patchResult === 'string' ? patchResult : file.contents
+ };
+ }
+ return file;
+ });
+
+ return repairedFiles;
+ } catch (error) {
+ if (error instanceof Error) {
+ console.error('Benchify Processing Error:', {
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ cause: error.cause
+ });
+ } else {
+ console.error('Unknown Benchify Error:', error);
+ }
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/lib/e2b.ts b/lib/e2b.ts
new file mode 100644
index 0000000..9f2bc1f
--- /dev/null
+++ b/lib/e2b.ts
@@ -0,0 +1,199 @@
+// lib/e2b.ts
+import { Sandbox } from '@e2b/sdk';
+import { GeneratedFile, DeployResult } from '@/lib/types';
+
+
+const E2B_API_KEY = process.env.E2B_API_KEY;
+
+if (!E2B_API_KEY) {
+ throw new Error('E2B_API_KEY is not set');
+}
+
+// Ensure path has a leading slash
+function normalizePath(path: string): string {
+ return path.startsWith('/') ? path : `/${path}`;
+}
+
+// Initialize E2B SDK
+export async function createSandbox() {
+ try {
+ const sandbox = await Sandbox.create({
+ apiKey: E2B_API_KEY,
+ });
+
+ return sandbox;
+ } catch (error: any) {
+ console.error('E2B Error Details:', {
+ message: error.message,
+ status: error.status,
+ statusText: error.statusText,
+ data: error.data,
+ headers: error.headers,
+ url: error.url
+ });
+ throw error;
+ }
+}
+
+// Set up a Vue environment with required dependencies
+export async function prepareVueEnvironment(sandbox: Sandbox) {
+ try {
+ // Create necessary directories
+ await sandbox.filesystem.makeDir('/src');
+ await sandbox.filesystem.makeDir('/src/components');
+
+ // Define base configuration files
+ const packageJson = {
+ name: "vue-app",
+ version: "1.0.0",
+ type: "module",
+ scripts: {
+ "dev": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview"
+ },
+ dependencies: {
+ "vue": "^3.3.0",
+ "vue-router": "^4.2.0",
+ "pinia": "^2.1.0",
+ "@vueuse/core": "^10.5.0"
+ },
+ devDependencies: {
+ "@vitejs/plugin-vue": "^4.4.0",
+ "typescript": "^5.2.0",
+ "vite": "^4.5.0",
+ "vue-tsc": "^1.8.0",
+ "tailwindcss": "^3.3.0",
+ "postcss": "^8.4.0",
+ "autoprefixer": "^10.4.0"
+ }
+ };
+
+ // Write initial configuration
+ await sandbox.filesystem.write('/package.json', JSON.stringify(packageJson, null, 2));
+
+ await sandbox.filesystem.write('/vite.config.ts', `import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+ server: {
+ host: true,
+ port: 3000
+ }
+})`);
+
+ // Install dependencies with legacy peer deps to avoid conflicts
+ await sandbox.process.start({
+ cmd: 'npm install --legacy-peer-deps',
+ });
+
+ // Write Vue app files
+ await sandbox.filesystem.write('/src/App.vue', `
+
+
+
+
+
+`);
+
+ await sandbox.filesystem.write('/tailwind.config.js', `/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}`);
+
+ await sandbox.filesystem.write('/postcss.config.js', `export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}`);
+
+ // Create index files
+ await sandbox.filesystem.write('/index.html', `
+
+
+
+
+ Vue App
+
+
+
+
+
+`);
+
+ await sandbox.filesystem.write('/src/main.ts', `import { createApp } from 'vue'
+import App from './App.vue'
+import './style.css'
+
+createApp(App).mount('#app')`);
+
+ await sandbox.filesystem.write('/src/style.css', `@tailwind base;
+@tailwind components;
+@tailwind utilities;`);
+
+ return sandbox;
+ } catch (error: any) {
+ console.error('E2B Environment Setup Error:', {
+ message: error.message,
+ details: error.details,
+ command: error.command,
+ exitCode: error.exitCode,
+ stdout: error.stdout,
+ stderr: error.stderr
+ });
+ throw error;
+ }
+}
+
+// Deploy the app for preview
+export async function deployApp(sandbox: Sandbox, files: GeneratedFile[]): Promise {
+ try {
+ // Write all the generated files
+ for (const file of files) {
+ const normalizedPath = normalizePath(file.path);
+ const dirPath = normalizedPath.split('/').slice(0, -1).join('/');
+
+ if (dirPath && dirPath !== '/') {
+ await sandbox.filesystem.makeDir(dirPath);
+ }
+
+ await sandbox.filesystem.write(normalizedPath, file.contents);
+ }
+
+ console.log('Starting development server...');
+ // Start the development server
+ const process = await sandbox.process.start({
+ cmd: 'npm run dev',
+ });
+
+ const previewUrl = `https://${sandbox.id}-3000.code.e2b.dev`;
+ console.log('Preview URL generated:', previewUrl);
+
+ // Return the URL for preview
+ return {
+ previewUrl,
+ process,
+ };
+ } catch (error: any) {
+ console.error('E2B Deployment Error:', {
+ message: error.message,
+ sandboxId: sandbox?.id,
+ status: error.status,
+ statusText: error.statusText,
+ data: error.data,
+ url: error.url
+ });
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/lib/openai.ts b/lib/openai.ts
new file mode 100644
index 0000000..196c66c
--- /dev/null
+++ b/lib/openai.ts
@@ -0,0 +1,53 @@
+// lib/openai.ts
+import { streamObject } from 'ai';
+import { openai } from '@ai-sdk/openai';
+import { z } from 'zod';
+import { VUE_APP_SYSTEM_PROMPT, VUE_APP_USER_PROMPT, TEMPERATURE, MODEL } from './prompts';
+
+const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
+
+if (!OPENAI_API_KEY) {
+ throw new Error('OPENAI_API_KEY is not set');
+}
+
+// Schema for a single file
+const fileSchema = z.object({
+ path: z.string(),
+ contents: z.string()
+});
+
+// Generate a Vue application using AI SDK
+export async function generateApp(
+ description: string,
+): Promise> {
+ console.log("Creating app with description: ", description);
+
+ try {
+ const { elementStream } = streamObject({
+ model: openai(MODEL),
+ output: 'array',
+ schema: fileSchema,
+ temperature: TEMPERATURE,
+ messages: [
+ { role: 'system', content: VUE_APP_SYSTEM_PROMPT },
+ { role: 'user', content: VUE_APP_USER_PROMPT(description) }
+ ]
+ });
+
+ const files = [];
+ for await (const file of elementStream) {
+ files.push(file);
+ }
+
+ if (!files.length) {
+ throw new Error("Failed to generate files - received empty response");
+ }
+
+ console.log("Generated files: ", files);
+
+ return files;
+ } catch (error) {
+ console.error('Error generating app:', error);
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/lib/prompts.ts b/lib/prompts.ts
new file mode 100644
index 0000000..d6ce0b5
--- /dev/null
+++ b/lib/prompts.ts
@@ -0,0 +1,29 @@
+// lib/prompts.ts
+
+export const VUE_APP_SYSTEM_PROMPT = `You are an expert Vue.js and Tailwind CSS developer.
+You will be generating a complete Vue 3 application based on the provided description.
+Follow these guidelines:
+- Use Vue 3 Composition API with