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 ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ const [result, setResult] = useState(null); -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- -
+ 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 ( +
+ + ( + + What would you like to build? + +