Spaces:
Running
Running
Update app/(main)/page.tsx
#7
by
In4ctividad
- opened
- app/(main)/page.tsx +70 -183
app/(main)/page.tsx
CHANGED
@@ -4,100 +4,73 @@ 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,
|
16 |
}
|
17 |
|
18 |
export default function Home() {
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
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 |
-
});
|
43 |
-
let [ref, scrollTo] = useScrollTo();
|
44 |
-
let [messages, setMessages] = useState<{ role: string; content: string }[]>(
|
45 |
-
[],
|
46 |
-
);
|
47 |
|
48 |
-
|
49 |
|
50 |
async function createApp(e: FormEvent<HTMLFormElement>) {
|
51 |
e.preventDefault();
|
52 |
-
|
53 |
-
if (status !== "initial") {
|
54 |
-
scrollTo({ delay: 0.5 });
|
55 |
-
}
|
56 |
-
|
57 |
setStatus("creating");
|
58 |
setGeneratedCode("");
|
59 |
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
"Content-Type": "application/json",
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
})
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
if (!res.body) {
|
76 |
-
throw new Error("No response body");
|
77 |
-
}
|
78 |
|
79 |
-
|
80 |
-
|
81 |
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
break;
|
|
|
86 |
}
|
87 |
-
|
88 |
const cleanedData = removeCodeFormatting(receivedData);
|
89 |
setGeneratedCode(cleanedData);
|
|
|
|
|
|
|
|
|
90 |
}
|
91 |
-
|
92 |
-
setMessages([{ role: "user", content: prompt }]);
|
93 |
-
setInitialAppConfig({ model });
|
94 |
-
setStatus("created");
|
95 |
}
|
96 |
|
97 |
useEffect(() => {
|
98 |
-
|
99 |
if (el && loading) {
|
100 |
-
|
101 |
el.scrollTo({ top: end });
|
102 |
}
|
103 |
}, [loading, generatedCode]);
|
@@ -105,15 +78,13 @@ export default function Home() {
|
|
105 |
return (
|
106 |
<main className="mt-12 flex w-full flex-1 flex-col items-center px-4 text-center sm:mt-1">
|
107 |
<a
|
108 |
-
className="mb-4 inline-flex h-7
|
109 |
href="https://ai.google.dev/gemini-api/docs"
|
110 |
target="_blank"
|
111 |
>
|
112 |
-
<span className="
|
113 |
-
Powered by <span className="font-medium">Gemini API</span>
|
114 |
-
</span>
|
115 |
</a>
|
116 |
-
<h1 className="my-6 max-w-3xl text-4xl font-bold text-gray-800
|
117 |
Turn your <span className="text-blue-600">idea</span>
|
118 |
<br /> into an <span className="text-blue-600">app</span>
|
119 |
</h1>
|
@@ -121,130 +92,46 @@ export default function Home() {
|
|
121 |
<form className="w-full max-w-xl" onSubmit={createApp}>
|
122 |
<fieldset disabled={loading} className="disabled:opacity-75">
|
123 |
<div className="relative mt-5">
|
124 |
-
<div className="
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
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 dark:text-gray-100 dark:placeholder-gray-400"
|
134 |
-
placeholder="Build me a calculator app..."
|
135 |
-
/>
|
136 |
-
</div>
|
137 |
<button
|
138 |
type="submit"
|
139 |
disabled={loading}
|
140 |
-
className="
|
141 |
>
|
142 |
-
{status === "creating" ?
|
143 |
-
<LoadingDots color="black" style="large" />
|
144 |
-
) : (
|
145 |
-
<ArrowLongRightIcon className="-ml-0.5 size-6" />
|
146 |
-
)}
|
147 |
</button>
|
148 |
</div>
|
149 |
</div>
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
</Select.Icon>
|
164 |
-
</Select.Trigger>
|
165 |
-
<Select.Portal>
|
166 |
-
<Select.Content className="overflow-hidden rounded-md bg-white dark:bg-[#1E293B] shadow-lg">
|
167 |
-
<Select.Viewport className="p-2">
|
168 |
-
{models.map((model) => (
|
169 |
-
<Select.Item
|
170 |
-
key={model.value}
|
171 |
-
value={model.value}
|
172 |
-
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-sm data-[highlighted]:bg-gray-100 dark:data-[highlighted]:bg-gray-800 data-[highlighted]:outline-none"
|
173 |
-
>
|
174 |
-
<Select.ItemText asChild>
|
175 |
-
<span className="inline-flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
176 |
-
<div className="size-2 rounded-full bg-green-500" />
|
177 |
-
{model.label}
|
178 |
-
</span>
|
179 |
-
</Select.ItemText>
|
180 |
-
<Select.ItemIndicator className="ml-auto">
|
181 |
-
<CheckIcon className="size-5 text-blue-600" />
|
182 |
-
</Select.ItemIndicator>
|
183 |
-
</Select.Item>
|
184 |
-
))}
|
185 |
-
</Select.Viewport>
|
186 |
-
<Select.ScrollDownButton />
|
187 |
-
<Select.Arrow />
|
188 |
-
</Select.Content>
|
189 |
-
</Select.Portal>
|
190 |
-
</Select.Root>
|
191 |
-
</div>
|
192 |
</div>
|
193 |
</fieldset>
|
194 |
</form>
|
195 |
|
196 |
-
<hr className="border-1 mb-20 h-px bg-gray-700 dark:bg-gray-700/30" />
|
197 |
-
|
198 |
{status !== "initial" && (
|
199 |
-
<motion.div
|
200 |
-
|
201 |
-
animate={{
|
202 |
-
height: "auto",
|
203 |
-
overflow: "hidden",
|
204 |
-
transitionEnd: { overflow: "visible" },
|
205 |
-
}}
|
206 |
-
transition={{ type: "spring", bounce: 0, duration: 0.5 }}
|
207 |
-
className="w-full pb-[25vh] pt-1"
|
208 |
-
onAnimationComplete={() => scrollTo()}
|
209 |
-
ref={ref}
|
210 |
-
>
|
211 |
-
<div className="relative mt-8 w-full overflow-hidden">
|
212 |
-
<div className="isolate">
|
213 |
-
<CodeViewer code={generatedCode} showEditor />
|
214 |
-
</div>
|
215 |
-
|
216 |
-
<AnimatePresence>
|
217 |
-
{loading && (
|
218 |
-
<motion.div
|
219 |
-
initial={status === "updating" ? { x: "100%" } : undefined}
|
220 |
-
animate={status === "updating" ? { x: "0%" } : undefined}
|
221 |
-
exit={{ x: "100%" }}
|
222 |
-
transition={{
|
223 |
-
type: "spring",
|
224 |
-
bounce: 0,
|
225 |
-
duration: 0.85,
|
226 |
-
delay: 0.5,
|
227 |
-
}}
|
228 |
-
className="absolute inset-x-0 bottom-0 top-1/2 flex items-center justify-center rounded-r border border-gray-400 dark:border-gray-700 bg-gradient-to-br from-gray-100 to-gray-300 dark:from-[#1E293B] dark:to-gray-800 md:inset-y-0 md:left-1/2 md:right-0"
|
229 |
-
>
|
230 |
-
<p className="animate-pulse text-3xl font-bold dark:text-gray-100">
|
231 |
-
{status === "creating"
|
232 |
-
? "Building your app..."
|
233 |
-
: "Updating your app..."}
|
234 |
-
</p>
|
235 |
-
</motion.div>
|
236 |
-
)}
|
237 |
-
</AnimatePresence>
|
238 |
-
</div>
|
239 |
</motion.div>
|
240 |
)}
|
241 |
</main>
|
242 |
);
|
243 |
}
|
244 |
-
|
245 |
-
async function minDelay<T>(promise: Promise<T>, ms: number) {
|
246 |
-
let delay = new Promise((resolve) => setTimeout(resolve, ms));
|
247 |
-
let [p] = await Promise.all([promise, delay]);
|
248 |
-
|
249 |
-
return p;
|
250 |
-
}
|
|
|
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 { AnimatePresence, motion } from "framer-motion";
|
8 |
import { FormEvent, useEffect, useState } from "react";
|
9 |
import LoadingDots from "../../components/loading-dots";
|
10 |
+
import * as Select from "@radix-ui/react-select";
|
11 |
|
12 |
function removeCodeFormatting(code: string): string {
|
13 |
+
return code.replace(/```(?:typescript|javascript|tsx)?\n([\s\S]*?)```/g, "$1").trim();
|
14 |
}
|
15 |
|
16 |
export default function Home() {
|
17 |
+
const [status, setStatus] = useState<"initial" | "creating" | "created">("initial");
|
18 |
+
const [prompt, setPrompt] = useState("");
|
19 |
+
const [model, setModel] = useState("gemini-2.0-flash-exp");
|
20 |
+
const [generatedCode, setGeneratedCode] = useState("");
|
21 |
+
const [ref, scrollTo] = useScrollTo();
|
22 |
+
|
23 |
+
const models = [
|
24 |
+
{ label: "gemini-2.0-flash-exp", value: "gemini-2.0-flash-exp" },
|
25 |
+
{ label: "gemini-1.5-pro", value: "gemini-1.5-pro" },
|
26 |
+
{ label: "gemini-1.5-flash", value: "gemini-1.5-flash" },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
+
const loading = status === "creating";
|
30 |
|
31 |
async function createApp(e: FormEvent<HTMLFormElement>) {
|
32 |
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
33 |
setStatus("creating");
|
34 |
setGeneratedCode("");
|
35 |
|
36 |
+
try {
|
37 |
+
const res = await fetch("/api/generateCode", {
|
38 |
+
method: "POST",
|
39 |
+
headers: { "Content-Type": "application/json" },
|
40 |
+
body: JSON.stringify({
|
41 |
+
model,
|
42 |
+
messages: [{ role: "user", content: prompt }],
|
43 |
+
}),
|
44 |
+
});
|
45 |
+
|
46 |
+
if (!res.ok) {
|
47 |
+
const errorBody = await res.text();
|
48 |
+
throw new Error(`HTTP Error ${res.status}: ${res.statusText}. ${errorBody}`);
|
49 |
+
}
|
|
|
|
|
|
|
|
|
50 |
|
51 |
+
const reader = res.body?.getReader();
|
52 |
+
if (!reader) throw new Error("The response does not contain a body.");
|
53 |
|
54 |
+
let receivedData = "";
|
55 |
+
while (true) {
|
56 |
+
const { done, value } = await reader.read();
|
57 |
+
if (done) break;
|
58 |
+
receivedData += new TextDecoder().decode(value);
|
59 |
}
|
60 |
+
|
61 |
const cleanedData = removeCodeFormatting(receivedData);
|
62 |
setGeneratedCode(cleanedData);
|
63 |
+
setStatus("created");
|
64 |
+
} catch (error: any) {
|
65 |
+
console.error("Error during code generation:", error.message);
|
66 |
+
setStatus("initial");
|
67 |
}
|
|
|
|
|
|
|
|
|
68 |
}
|
69 |
|
70 |
useEffect(() => {
|
71 |
+
const el = document.querySelector(".cm-scroller");
|
72 |
if (el && loading) {
|
73 |
+
const end = el.scrollHeight - el.clientHeight;
|
74 |
el.scrollTo({ top: end });
|
75 |
}
|
76 |
}, [loading, generatedCode]);
|
|
|
78 |
return (
|
79 |
<main className="mt-12 flex w-full flex-1 flex-col items-center px-4 text-center sm:mt-1">
|
80 |
<a
|
81 |
+
className="mb-4 inline-flex h-7 items-center rounded-3xl bg-gray-300/50 px-7 py-5 shadow-sm dark:bg-gray-800/50"
|
82 |
href="https://ai.google.dev/gemini-api/docs"
|
83 |
target="_blank"
|
84 |
>
|
85 |
+
Powered by <span className="font-medium">Gemini API</span>
|
|
|
|
|
86 |
</a>
|
87 |
+
<h1 className="my-6 max-w-3xl text-4xl font-bold text-gray-800 sm:text-6xl">
|
88 |
Turn your <span className="text-blue-600">idea</span>
|
89 |
<br /> into an <span className="text-blue-600">app</span>
|
90 |
</h1>
|
|
|
92 |
<form className="w-full max-w-xl" onSubmit={createApp}>
|
93 |
<fieldset disabled={loading} className="disabled:opacity-75">
|
94 |
<div className="relative mt-5">
|
95 |
+
<div className="relative flex bg-white shadow-md rounded-xl">
|
96 |
+
<textarea
|
97 |
+
rows={3}
|
98 |
+
required
|
99 |
+
value={prompt}
|
100 |
+
onChange={(e) => setPrompt(e.target.value)}
|
101 |
+
className="w-full resize-none rounded-l-xl p-4"
|
102 |
+
placeholder="Build me a calculator app..."
|
103 |
+
/>
|
|
|
|
|
|
|
|
|
104 |
<button
|
105 |
type="submit"
|
106 |
disabled={loading}
|
107 |
+
className="bg-blue-500 text-white px-5 rounded-r-xl"
|
108 |
>
|
109 |
+
{status === "creating" ? <LoadingDots /> : <ArrowLongRightIcon />}
|
|
|
|
|
|
|
|
|
110 |
</button>
|
111 |
</div>
|
112 |
</div>
|
113 |
+
|
114 |
+
<div className="mt-6 flex items-center gap-4">
|
115 |
+
<label className="text-gray-500">Model:</label>
|
116 |
+
<Select.Root value={model} onValueChange={(value) => setModel(value)}>
|
117 |
+
<Select.Trigger className="p-2 bg-gray-100 rounded-md">{model}</Select.Trigger>
|
118 |
+
<Select.Content>
|
119 |
+
{models.map((model) => (
|
120 |
+
<Select.Item key={model.value} value={model.value}>
|
121 |
+
{model.label}
|
122 |
+
</Select.Item>
|
123 |
+
))}
|
124 |
+
</Select.Content>
|
125 |
+
</Select.Root>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
</div>
|
127 |
</fieldset>
|
128 |
</form>
|
129 |
|
|
|
|
|
130 |
{status !== "initial" && (
|
131 |
+
<motion.div initial={{ height: 0 }} animate={{ height: "auto" }} className="w-full">
|
132 |
+
<CodeViewer code={generatedCode} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
</motion.div>
|
134 |
)}
|
135 |
</main>
|
136 |
);
|
137 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|