Demo
This commit is contained in:
66
app/api/generate-code-from-image/route.ts
Normal file
66
app/api/generate-code-from-image/route.ts
Normal file
@ -0,0 +1,66 @@
|
||||
// ./app/api/chat/route.js
|
||||
import OpenAI from 'openai'
|
||||
import { OpenAIStream, StreamingTextResponse } from 'ai'
|
||||
|
||||
const USER_PROMPT = 'Generate code a web page that looks exactly like this'
|
||||
|
||||
const SYSTEM_PROMPT= `You are an expert Tailwind developer
|
||||
You take screenshots of a reference web page from the user, and then build single page apps
|
||||
using Tailwind, HTML and JS.
|
||||
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
|
||||
update it to look more like the reference image(The first image).
|
||||
|
||||
- Make sure the app looks exactly like the screenshot.
|
||||
- Pay close attention to background color, text color, font size, font family,
|
||||
padding, margin, border, etc. Match the colors and sizes exactly.
|
||||
- Use the exact text from the screenshot.
|
||||
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
|
||||
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
|
||||
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
|
||||
|
||||
In terms of libraries,
|
||||
|
||||
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
|
||||
- You can use Google Fonts
|
||||
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
|
||||
|
||||
Return only the full code in <html></html> tags.
|
||||
Do not include markdown "\`\`\`" or "\`\`\`html" at the start or end.`
|
||||
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
})
|
||||
|
||||
export const runtime = 'edge' //vs cloud. Edge is faster but more expensive
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { url } = await req.json()
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4-vision-preview',
|
||||
stream: true,
|
||||
max_tokens: 4096,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: SYSTEM_PROMPT,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: USER_PROMPT,
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: url
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
const stream = OpenAIStream(response)
|
||||
return new StreamingTextResponse(stream)
|
||||
}
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
28
app/form.tsx
Normal file
28
app/form.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { FormEvent } from 'react'
|
||||
|
||||
export const Form = ({transformUrlToCode} : {transformUrlToCode: (url: string) => void}) =>{
|
||||
|
||||
const handleSubmit = (evt: FormEvent) => {
|
||||
const form = evt.currentTarget as HTMLFormElement;
|
||||
const url = form.elements.namedItem('url') as HTMLInputElement;
|
||||
transformUrlToCode(url.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-5"
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault()
|
||||
handleSubmit(evt)
|
||||
}}
|
||||
>
|
||||
<Label htmlFor="Introduce to URL de la imagen" />
|
||||
<Input type="text" name="url" id="url" placeholder="https://tu-screnshot.jpg"/>
|
||||
<Button>Generar imágen</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
76
app/globals.css
Normal file
76
app/globals.css
Normal file
@ -0,0 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
22
app/layout.tsx
Normal file
22
app/layout.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="es">
|
||||
<body className={`${inter.className} dark`}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
88
app/page.tsx
Normal file
88
app/page.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Form } from './form'
|
||||
import { useState } from 'react'
|
||||
|
||||
const STEPS = {
|
||||
INITIAL: 'INITIAL',
|
||||
LOADING: 'LOADING',
|
||||
PREVIEW: 'PREVIEW',
|
||||
ERROR: 'ERROR',
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [result, setResult] = useState('')
|
||||
const [step, setStep] = useState(STEPS.INITIAL)
|
||||
|
||||
const transformUrlToCode = async (url: string) => {
|
||||
setStep(STEPS.LOADING)
|
||||
const res = await fetch('/api/generate-code-from-image', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ url }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!res.ok || res.body === null) {
|
||||
throw new Error('Error al generar el código')
|
||||
setStep(STEPS.ERROR)
|
||||
}
|
||||
|
||||
setStep(STEPS.PREVIEW)
|
||||
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
const chunk = decoder.decode(value)
|
||||
|
||||
setResult((prevResult) => prevResult + chunk)
|
||||
|
||||
console.log(chunk)
|
||||
if (done) break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[400px_1fr]">
|
||||
<aside className="flex flex-col justify-between min-h-scren p-4 bg-gray-900">
|
||||
<header className="text-center">
|
||||
<h1 className="text-3xl font-semibold">IMAGE 2 CODE</h1>
|
||||
<h2 className="text-sm opacity-75">Pasa tus imágenes a código en segundos</h2>
|
||||
</header>
|
||||
<section>{/* Aquí irán los filtros... */}</section>
|
||||
<footer>Desarrollado por rdev</footer>
|
||||
</aside>
|
||||
<main className="bg-gray-950">
|
||||
<section className="max-w-5xl w-full mx-auto p-10">
|
||||
{step == STEPS.INITIAL && (
|
||||
<div>
|
||||
<Form transformUrlToCode={transformUrlToCode} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == STEPS.LOADING && (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h2 className="text-2xl font-semibold">Generando código...</h2>
|
||||
<div className="w-24 h-24 border-4 border-gray-900 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == STEPS.PREVIEW && (
|
||||
<div className="rounded flex flex-col gap-4">
|
||||
<iframe srcDoc={result} className="w-full h-full border-4 rounded border-gray-700 aspect-video" />
|
||||
<pre className='pt-10'>
|
||||
<code>{result}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == STEPS.ERROR && 'ERROR'}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user