Update app/(main)/page.tsx

#7
Files changed (1) hide show
  1. 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, '$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
- });
43
- let [ref, scrollTo] = useScrollTo();
44
- let [messages, setMessages] = useState<{ role: string; content: string }[]>(
45
- [],
46
- );
47
 
48
- let loading = status === "creating" || status === "updating";
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
- let res = await fetch("/api/generateCode", {
61
- method: "POST",
62
- headers: {
63
- "Content-Type": "application/json",
64
- },
65
- body: JSON.stringify({
66
- model,
67
- messages: [{ role: "user", content: prompt }],
68
- }),
69
- });
70
-
71
- if (!res.ok) {
72
- throw new Error(res.statusText);
73
- }
74
-
75
- if (!res.body) {
76
- throw new Error("No response body");
77
- }
78
 
79
- const reader = res.body.getReader();
80
- let receivedData = "";
81
 
82
- while (true) {
83
- const { done, value } = await reader.read();
84
- if (done) {
85
- break;
 
86
  }
87
- receivedData += new TextDecoder().decode(value);
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
- let el = document.querySelector(".cm-scroller");
99
  if (el && loading) {
100
- let end = el.scrollHeight - el.clientHeight;
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 shrink-0 items-center gap-[9px] rounded-[50px] border-[0.5px] border-solid border-[#E6E6E6] bg-[rgba(234,238,255,0.65)] dark:bg-[rgba(30,41,59,0.5)] dark:border-gray-700 px-7 py-5 shadow-[0px_1px_1px_0px_rgba(0,0,0,0.25)]"
109
  href="https://ai.google.dev/gemini-api/docs"
110
  target="_blank"
111
  >
112
- <span className="text-center">
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 dark:text-white sm:text-6xl">
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="absolute -inset-2 rounded-[32px] bg-gray-300/50 dark:bg-gray-800/50" />
125
- <div className="relative flex rounded-3xl bg-white dark:bg-[#1E293B] shadow-sm">
126
- <div className="relative flex flex-grow items-stretch focus-within:z-10">
127
- <textarea
128
- rows={3}
129
- required
130
- value={prompt}
131
- onChange={(e) => setPrompt(e.target.value)}
132
- name="prompt"
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="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 dark:disabled:text-gray-400"
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
- <div className="mt-6 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:gap-8">
151
- <div className="flex items-center justify-between gap-3 sm:justify-center">
152
- <p className="text-gray-500 dark:text-gray-400 sm:text-xs">Model:</p>
153
- <Select.Root
154
- name="model"
155
- disabled={loading}
156
- value={model}
157
- onValueChange={(value) => setModel(value)}
158
- >
159
- <Select.Trigger className="group flex w-60 max-w-xs items-center rounded-2xl border-[6px] border-gray-300 dark:border-gray-700 bg-white dark:bg-[#1E293B] px-4 py-2 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500">
160
- <Select.Value />
161
- <Select.Icon className="ml-auto">
162
- <ChevronDownIcon className="size-6 text-gray-300 group-focus-visible:text-gray-500 group-enabled:group-hover:text-gray-500 dark:text-gray-600 dark:group-focus-visible:text-gray-400 dark:group-enabled:group-hover:text-gray-400" />
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
- initial={{ height: 0 }}
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
  }