Spaces:
Running
Running
osanseviero
commited on
Commit
•
a94c081
1
Parent(s):
c515dc7
Upload 2 files
Browse files
page.tsx
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import CodeViewer from "@/components/code-viewer";
|
4 |
+
import { useScrollTo } from "@/hooks/use-scroll-to";
|
5 |
+
import { CheckIcon } from "@heroicons/react/16/solid";
|
6 |
+
import { ArrowLongRightIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
7 |
+
import { ArrowUpOnSquareIcon } from "@heroicons/react/24/outline";
|
8 |
+
import * as Select from "@radix-ui/react-select";
|
9 |
+
import * as Switch from "@radix-ui/react-switch";
|
10 |
+
import { AnimatePresence, motion } from "framer-motion";
|
11 |
+
import { FormEvent, useEffect, useState } from "react";
|
12 |
+
import LoadingDots from "../../components/loading-dots";
|
13 |
+
|
14 |
+
function removeCodeFormatting(code: string): string {
|
15 |
+
return code.replace(/```(?:typescript|javascript|tsx)?\n([\s\S]*?)```/g, '$1').trim();
|
16 |
+
}
|
17 |
+
|
18 |
+
export default function Home() {
|
19 |
+
let [status, setStatus] = useState<
|
20 |
+
"initial" | "creating" | "created" | "updating" | "updated"
|
21 |
+
>("initial");
|
22 |
+
let [prompt, setPrompt] = useState("");
|
23 |
+
let models = [
|
24 |
+
{
|
25 |
+
label: "gemini-2.0-flash-exp",
|
26 |
+
value: "gemini-2.0-flash-exp",
|
27 |
+
},
|
28 |
+
{
|
29 |
+
label: "gemini-1.5-pro",
|
30 |
+
value: "gemini-1.5-pro",
|
31 |
+
},
|
32 |
+
{
|
33 |
+
label: "gemini-1.5-flash",
|
34 |
+
value: "gemini-1.5-flash",
|
35 |
+
}
|
36 |
+
];
|
37 |
+
let [model, setModel] = useState(models[0].value);
|
38 |
+
let [modification, setModification] = useState("");
|
39 |
+
let [generatedCode, setGeneratedCode] = useState("");
|
40 |
+
let [initialAppConfig, setInitialAppConfig] = useState({
|
41 |
+
model: "",
|
42 |
+
shadcn: true,
|
43 |
+
});
|
44 |
+
let [ref, scrollTo] = useScrollTo();
|
45 |
+
let [messages, setMessages] = useState<{ role: string; content: string }[]>(
|
46 |
+
[],
|
47 |
+
);
|
48 |
+
|
49 |
+
let loading = status === "creating" || status === "updating";
|
50 |
+
|
51 |
+
async function createApp(e: FormEvent<HTMLFormElement>) {
|
52 |
+
e.preventDefault();
|
53 |
+
|
54 |
+
if (status !== "initial") {
|
55 |
+
scrollTo({ delay: 0.5 });
|
56 |
+
}
|
57 |
+
|
58 |
+
setStatus("creating");
|
59 |
+
setGeneratedCode("");
|
60 |
+
|
61 |
+
let res = await fetch("/api/generateCode", {
|
62 |
+
method: "POST",
|
63 |
+
headers: {
|
64 |
+
"Content-Type": "application/json",
|
65 |
+
},
|
66 |
+
body: JSON.stringify({
|
67 |
+
model,
|
68 |
+
messages: [{ role: "user", content: prompt }],
|
69 |
+
}),
|
70 |
+
});
|
71 |
+
|
72 |
+
if (!res.ok) {
|
73 |
+
throw new Error(res.statusText);
|
74 |
+
}
|
75 |
+
|
76 |
+
if (!res.body) {
|
77 |
+
throw new Error("No response body");
|
78 |
+
}
|
79 |
+
|
80 |
+
const reader = res.body.getReader();
|
81 |
+
let receivedData = "";
|
82 |
+
|
83 |
+
while (true) {
|
84 |
+
const { done, value } = await reader.read();
|
85 |
+
if (done) {
|
86 |
+
break;
|
87 |
+
}
|
88 |
+
receivedData += new TextDecoder().decode(value);
|
89 |
+
const cleanedData = removeCodeFormatting(receivedData);
|
90 |
+
setGeneratedCode(cleanedData);
|
91 |
+
}
|
92 |
+
|
93 |
+
setMessages([{ role: "user", content: prompt }]);
|
94 |
+
setInitialAppConfig({ model });
|
95 |
+
setStatus("created");
|
96 |
+
}
|
97 |
+
|
98 |
+
useEffect(() => {
|
99 |
+
let el = document.querySelector(".cm-scroller");
|
100 |
+
if (el && loading) {
|
101 |
+
let end = el.scrollHeight - el.clientHeight;
|
102 |
+
el.scrollTo({ top: end });
|
103 |
+
}
|
104 |
+
}, [loading, generatedCode]);
|
105 |
+
|
106 |
+
return (
|
107 |
+
<main className="mt-12 flex w-full flex-1 flex-col items-center px-4 text-center sm:mt-1">
|
108 |
+
<a
|
109 |
+
className="mb-4 inline-flex h-7 shrink-0 items-center gap-[9px] rounded-[50px] border-[0.5px] border-solid border-[#E6E6E6] bg-[rgba(234,238,255,0.65)] bg-gray-100 px-7 py-5 shadow-[0px_1px_1px_0px_rgba(0,0,0,0.25)]"
|
110 |
+
href="https://ai.google.dev/gemini-api/docs"
|
111 |
+
target="_blank"
|
112 |
+
>
|
113 |
+
<span className="text-center">
|
114 |
+
Powered by <span className="font-medium">Gemini API</span>
|
115 |
+
</span>
|
116 |
+
</a>
|
117 |
+
<h1 className="my-6 max-w-3xl text-4xl font-bold text-gray-800 sm:text-6xl">
|
118 |
+
Turn your <span className="text-blue-600">idea</span>
|
119 |
+
<br /> into an <span className="text-blue-600">app</span>
|
120 |
+
</h1>
|
121 |
+
|
122 |
+
<form className="w-full max-w-xl" onSubmit={createApp}>
|
123 |
+
<fieldset disabled={loading} className="disabled:opacity-75">
|
124 |
+
<div className="relative mt-5">
|
125 |
+
<div className="absolute -inset-2 rounded-[32px] bg-gray-300/50" />
|
126 |
+
<div className="relative flex rounded-3xl bg-white shadow-sm">
|
127 |
+
<div className="relative flex flex-grow items-stretch focus-within:z-10">
|
128 |
+
<textarea
|
129 |
+
rows={3}
|
130 |
+
required
|
131 |
+
value={prompt}
|
132 |
+
onChange={(e) => setPrompt(e.target.value)}
|
133 |
+
name="prompt"
|
134 |
+
className="w-full resize-none rounded-l-3xl bg-transparent px-6 py-5 text-lg focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500"
|
135 |
+
placeholder="Build me a calculator app..."
|
136 |
+
/>
|
137 |
+
</div>
|
138 |
+
<button
|
139 |
+
type="submit"
|
140 |
+
disabled={loading}
|
141 |
+
className="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-3xl px-3 py-2 text-sm font-semibold text-blue-500 hover:text-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500 disabled:text-gray-900"
|
142 |
+
>
|
143 |
+
{status === "creating" ? (
|
144 |
+
<LoadingDots color="black" style="large" />
|
145 |
+
) : (
|
146 |
+
<ArrowLongRightIcon className="-ml-0.5 size-6" />
|
147 |
+
)}
|
148 |
+
</button>
|
149 |
+
</div>
|
150 |
+
</div>
|
151 |
+
<div className="mt-6 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:gap-8">
|
152 |
+
<div className="flex items-center justify-between gap-3 sm:justify-center">
|
153 |
+
<p className="text-gray-500 sm:text-xs">Model:</p>
|
154 |
+
<Select.Root
|
155 |
+
name="model"
|
156 |
+
disabled={loading}
|
157 |
+
value={model}
|
158 |
+
onValueChange={(value) => setModel(value)}
|
159 |
+
>
|
160 |
+
<Select.Trigger className="group flex w-60 max-w-xs items-center rounded-2xl border-[6px] border-gray-300 bg-white px-4 py-2 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500">
|
161 |
+
<Select.Value />
|
162 |
+
<Select.Icon className="ml-auto">
|
163 |
+
<ChevronDownIcon className="size-6 text-gray-300 group-focus-visible:text-gray-500 group-enabled:group-hover:text-gray-500" />
|
164 |
+
</Select.Icon>
|
165 |
+
</Select.Trigger>
|
166 |
+
<Select.Portal>
|
167 |
+
<Select.Content className="overflow-hidden rounded-md bg-white shadow-lg">
|
168 |
+
<Select.Viewport className="p-2">
|
169 |
+
{models.map((model) => (
|
170 |
+
<Select.Item
|
171 |
+
key={model.value}
|
172 |
+
value={model.value}
|
173 |
+
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-sm data-[highlighted]:bg-gray-100 data-[highlighted]:outline-none"
|
174 |
+
>
|
175 |
+
<Select.ItemText asChild>
|
176 |
+
<span className="inline-flex items-center gap-2 text-gray-500">
|
177 |
+
<div className="size-2 rounded-full bg-green-500" />
|
178 |
+
{model.label}
|
179 |
+
</span>
|
180 |
+
</Select.ItemText>
|
181 |
+
<Select.ItemIndicator className="ml-auto">
|
182 |
+
<CheckIcon className="size-5 text-blue-600" />
|
183 |
+
</Select.ItemIndicator>
|
184 |
+
</Select.Item>
|
185 |
+
))}
|
186 |
+
</Select.Viewport>
|
187 |
+
<Select.ScrollDownButton />
|
188 |
+
<Select.Arrow />
|
189 |
+
</Select.Content>
|
190 |
+
</Select.Portal>
|
191 |
+
</Select.Root>
|
192 |
+
</div>
|
193 |
+
</div>
|
194 |
+
</fieldset>
|
195 |
+
</form>
|
196 |
+
|
197 |
+
<hr className="border-1 mb-20 h-px bg-gray-700 dark:bg-gray-700" />
|
198 |
+
|
199 |
+
{status !== "initial" && (
|
200 |
+
<motion.div
|
201 |
+
initial={{ height: 0 }}
|
202 |
+
animate={{
|
203 |
+
height: "auto",
|
204 |
+
overflow: "hidden",
|
205 |
+
transitionEnd: { overflow: "visible" },
|
206 |
+
}}
|
207 |
+
transition={{ type: "spring", bounce: 0, duration: 0.5 }}
|
208 |
+
className="w-full pb-[25vh] pt-1"
|
209 |
+
onAnimationComplete={() => scrollTo()}
|
210 |
+
ref={ref}
|
211 |
+
>
|
212 |
+
<div className="relative mt-8 w-full overflow-hidden">
|
213 |
+
<div className="isolate">
|
214 |
+
<CodeViewer code={generatedCode} showEditor />
|
215 |
+
</div>
|
216 |
+
|
217 |
+
<AnimatePresence>
|
218 |
+
{loading && (
|
219 |
+
<motion.div
|
220 |
+
initial={status === "updating" ? { x: "100%" } : undefined}
|
221 |
+
animate={status === "updating" ? { x: "0%" } : undefined}
|
222 |
+
exit={{ x: "100%" }}
|
223 |
+
transition={{
|
224 |
+
type: "spring",
|
225 |
+
bounce: 0,
|
226 |
+
duration: 0.85,
|
227 |
+
delay: 0.5,
|
228 |
+
}}
|
229 |
+
className="absolute inset-x-0 bottom-0 top-1/2 flex items-center justify-center rounded-r border border-gray-400 bg-gradient-to-br from-gray-100 to-gray-300 md:inset-y-0 md:left-1/2 md:right-0"
|
230 |
+
>
|
231 |
+
<p className="animate-pulse text-3xl font-bold">
|
232 |
+
{status === "creating"
|
233 |
+
? "Building your app..."
|
234 |
+
: "Updating your app..."}
|
235 |
+
</p>
|
236 |
+
</motion.div>
|
237 |
+
)}
|
238 |
+
</AnimatePresence>
|
239 |
+
</div>
|
240 |
+
</motion.div>
|
241 |
+
)}
|
242 |
+
</main>
|
243 |
+
);
|
244 |
+
}
|
245 |
+
|
246 |
+
async function minDelay<T>(promise: Promise<T>, ms: number) {
|
247 |
+
let delay = new Promise((resolve) => setTimeout(resolve, ms));
|
248 |
+
let [p] = await Promise.all([promise, delay]);
|
249 |
+
|
250 |
+
return p;
|
251 |
+
}
|
route.ts
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import dedent from "dedent";
|
2 |
+
import { z } from "zod";
|
3 |
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
4 |
+
|
5 |
+
const apiKey = process.env.GOOGLE_AI_API_KEY || "";
|
6 |
+
const genAI = new GoogleGenerativeAI(apiKey);
|
7 |
+
|
8 |
+
export async function POST(req: Request) {
|
9 |
+
let json = await req.json();
|
10 |
+
let result = z
|
11 |
+
.object({
|
12 |
+
model: z.string(),
|
13 |
+
messages: z.array(
|
14 |
+
z.object({
|
15 |
+
role: z.enum(["user", "assistant"]),
|
16 |
+
content: z.string(),
|
17 |
+
}),
|
18 |
+
),
|
19 |
+
})
|
20 |
+
.safeParse(json);
|
21 |
+
|
22 |
+
if (result.error) {
|
23 |
+
return new Response(result.error.message, { status: 422 });
|
24 |
+
}
|
25 |
+
|
26 |
+
let { model, messages } = result.data;
|
27 |
+
let systemPrompt = getSystemPrompt();
|
28 |
+
|
29 |
+
const geminiModel = genAI.getGenerativeModel({model: model});
|
30 |
+
|
31 |
+
const geminiStream = await geminiModel.generateContentStream(
|
32 |
+
messages[0].content + systemPrompt + "\nPlease ONLY return code, NO backticks or language names. Don't start with \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`."
|
33 |
+
);
|
34 |
+
|
35 |
+
const readableStream = new ReadableStream({
|
36 |
+
async start(controller) {
|
37 |
+
for await (const chunk of geminiStream.stream) {
|
38 |
+
const chunkText = chunk.text();
|
39 |
+
controller.enqueue(new TextEncoder().encode(chunkText));
|
40 |
+
}
|
41 |
+
controller.close();
|
42 |
+
},
|
43 |
+
});
|
44 |
+
|
45 |
+
return new Response(readableStream);
|
46 |
+
}
|
47 |
+
|
48 |
+
function getSystemPrompt() {
|
49 |
+
let systemPrompt =
|
50 |
+
`You are an expert frontend React engineer who is also a great UI/UX designer. Follow the instructions carefully, I will tip you $1 million if you do a good job:
|
51 |
+
|
52 |
+
- Think carefully step by step.
|
53 |
+
- Create a React component for whatever the user asked you to create and make sure it can run by itself by using a default export
|
54 |
+
- Make sure the React app is interactive and functional by creating state when needed and having no required props
|
55 |
+
- If you use any imports from React like useState or useEffect, make sure to import them directly
|
56 |
+
- Use TypeScript as the language for the React component
|
57 |
+
- Use Tailwind classes for styling. DO NOT USE ARBITRARY VALUES (e.g. \`h-[600px]\`). Make sure to use a consistent color palette.
|
58 |
+
- Use Tailwind margin and padding classes to style the components and ensure the components are spaced out nicely
|
59 |
+
- Please ONLY return the full React code starting with the imports, nothing else. It's very important for my job that you only return the React code with imports. DO NOT START WITH \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`.
|
60 |
+
- ONLY IF the user asks for a dashboard, graph or chart, the recharts library is available to be imported, e.g. \`import { LineChart, XAxis, ... } from "recharts"\` & \`<LineChart ...><XAxis dataKey="name"> ...\`. Please only use this when needed.
|
61 |
+
- For placeholder images, please use a <div className="bg-gray-200 border-2 border-dashed rounded-xl w-16 h-16" />
|
62 |
+
`;
|
63 |
+
|
64 |
+
systemPrompt += `
|
65 |
+
NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
|
66 |
+
`;
|
67 |
+
return dedent(systemPrompt);
|
68 |
+
}
|
69 |
+
|
70 |
+
export const runtime = "edge";
|