GemCoder / app /(main) /page.tsx
osanseviero's picture
Upload 54 files
63c7991 verified
raw
history blame
10.7 kB
"use client";
import CodeViewer from "@/components/code-viewer";
import { useScrollTo } from "@/hooks/use-scroll-to";
import { CheckIcon } from "@heroicons/react/16/solid";
import { ArrowLongRightIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
import { ArrowUpOnSquareIcon } from "@heroicons/react/24/outline";
import * as Select from "@radix-ui/react-select";
import * as Switch from "@radix-ui/react-switch";
import { AnimatePresence, motion } from "framer-motion";
import { FormEvent, useEffect, useState } from "react";
import LoadingDots from "../../components/loading-dots";
function removeCodeFormatting(code: string): string {
return code.replace(/```(?:typescript|javascript|tsx)?\n([\s\S]*?)```/g, '$1').trim();
}
export default function Home() {
let [status, setStatus] = useState<
"initial" | "creating" | "created" | "updating" | "updated"
>("initial");
let [prompt, setPrompt] = useState("");
let models = [
{
label: "gemini-2.0-flash-exp",
value: "gemini-2.0-flash-exp",
},
{
label: "gemini-1.5-pro",
value: "gemini-1.5-pro",
},
{
label: "gemini-1.5-flash",
value: "gemini-1.5-flash",
}
];
let [model, setModel] = useState(models[0].value);
let [shadcn, setShadcn] = useState(false);
let [modification, setModification] = useState("");
let [generatedCode, setGeneratedCode] = useState("");
let [initialAppConfig, setInitialAppConfig] = useState({
model: "",
shadcn: true,
});
let [ref, scrollTo] = useScrollTo();
let [messages, setMessages] = useState<{ role: string; content: string }[]>(
[],
);
let loading = status === "creating" || status === "updating";
async function createApp(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (status !== "initial") {
scrollTo({ delay: 0.5 });
}
setStatus("creating");
setGeneratedCode("");
let res = await fetch("/api/generateCode", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
shadcn,
messages: [{ role: "user", content: prompt }],
}),
});
if (!res.ok) {
throw new Error(res.statusText);
}
if (!res.body) {
throw new Error("No response body");
}
const reader = res.body.getReader();
let receivedData = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
receivedData += new TextDecoder().decode(value);
const cleanedData = removeCodeFormatting(receivedData);
setGeneratedCode(cleanedData);
}
setMessages([{ role: "user", content: prompt }]);
setInitialAppConfig({ model, shadcn });
setStatus("created");
}
useEffect(() => {
let el = document.querySelector(".cm-scroller");
if (el && loading) {
let end = el.scrollHeight - el.clientHeight;
el.scrollTo({ top: end });
}
}, [loading, generatedCode]);
return (
<main className="mt-12 flex w-full flex-1 flex-col items-center px-4 text-center sm:mt-1">
<a
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)]"
href="https://ai.google.dev/gemini-api/docs"
target="_blank"
>
<span className="text-center">
Powered by <span className="font-medium">Gemini API</span>
</span>
</a>
<h1 className="my-6 max-w-3xl text-4xl font-bold text-gray-800 sm:text-6xl">
Turn your <span className="text-blue-600">idea</span>
<br /> into an <span className="text-blue-600">app</span>
</h1>
<form className="w-full max-w-xl" onSubmit={createApp}>
<fieldset disabled={loading} className="disabled:opacity-75">
<div className="relative mt-5">
<div className="absolute -inset-2 rounded-[32px] bg-gray-300/50" />
<div className="relative flex rounded-3xl bg-white shadow-sm">
<div className="relative flex flex-grow items-stretch focus-within:z-10">
<textarea
rows={3}
required
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
name="prompt"
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"
placeholder="Build me a calculator app..."
/>
</div>
<button
type="submit"
disabled={loading}
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"
>
{status === "creating" ? (
<LoadingDots color="black" style="large" />
) : (
<ArrowLongRightIcon className="-ml-0.5 size-6" />
)}
</button>
</div>
</div>
<div className="mt-6 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:gap-8">
<div className="flex items-center justify-between gap-3 sm:justify-center">
<p className="text-gray-500 sm:text-xs">Model:</p>
<Select.Root
name="model"
disabled={loading}
value={model}
onValueChange={(value) => setModel(value)}
>
<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">
<Select.Value />
<Select.Icon className="ml-auto">
<ChevronDownIcon className="size-6 text-gray-300 group-focus-visible:text-gray-500 group-enabled:group-hover:text-gray-500" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="overflow-hidden rounded-md bg-white shadow-lg">
<Select.Viewport className="p-2">
{models.map((model) => (
<Select.Item
key={model.value}
value={model.value}
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-sm data-[highlighted]:bg-gray-100 data-[highlighted]:outline-none"
>
<Select.ItemText asChild>
<span className="inline-flex items-center gap-2 text-gray-500">
<div className="size-2 rounded-full bg-green-500" />
{model.label}
</span>
</Select.ItemText>
<Select.ItemIndicator className="ml-auto">
<CheckIcon className="size-5 text-blue-600" />
</Select.ItemIndicator>
</Select.Item>
))}
</Select.Viewport>
<Select.ScrollDownButton />
<Select.Arrow />
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
<div className="flex h-full items-center justify-between gap-3 sm:justify-center">
<label className="text-gray-500 sm:text-xs" htmlFor="shadcn">
shadcn/ui:
</label>
<Switch.Root
className="group flex w-20 max-w-xs items-center rounded-2xl border-[6px] border-gray-300 bg-white p-1.5 text-sm shadow-inner transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500 data-[state=checked]:bg-blue-500"
id="shadcn"
name="shadcn"
checked={shadcn}
onCheckedChange={(value) => setShadcn(value)}
>
<Switch.Thumb className="size-7 rounded-lg bg-gray-200 shadow-[0_1px_2px] shadow-gray-400 transition data-[state=checked]:translate-x-7 data-[state=checked]:bg-white data-[state=checked]:shadow-gray-600" />
</Switch.Root>
</div>
</div>
</fieldset>
</form>
<hr className="border-1 mb-20 h-px bg-gray-700 dark:bg-gray-700" />
{status !== "initial" && (
<motion.div
initial={{ height: 0 }}
animate={{
height: "auto",
overflow: "hidden",
transitionEnd: { overflow: "visible" },
}}
transition={{ type: "spring", bounce: 0, duration: 0.5 }}
className="w-full pb-[25vh] pt-1"
onAnimationComplete={() => scrollTo()}
ref={ref}
>
<div className="relative mt-8 w-full overflow-hidden">
<div className="isolate">
<CodeViewer code={generatedCode} showEditor />
</div>
<AnimatePresence>
{loading && (
<motion.div
initial={status === "updating" ? { x: "100%" } : undefined}
animate={status === "updating" ? { x: "0%" } : undefined}
exit={{ x: "100%" }}
transition={{
type: "spring",
bounce: 0,
duration: 0.85,
delay: 0.5,
}}
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"
>
<p className="animate-pulse text-3xl font-bold">
{status === "creating"
? "Building your app..."
: "Updating your app..."}
</p>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
)}
</main>
);
}
async function minDelay<T>(promise: Promise<T>, ms: number) {
let delay = new Promise((resolve) => setTimeout(resolve, ms));
let [p] = await Promise.all([promise, delay]);
return p;
}