Spaces:
Running
Running
Delete page.tsx
Browse files
page.tsx
DELETED
|
@@ -1,251 +0,0 @@
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|