File size: 4,926 Bytes
63c7991
 
 
 
 
 
 
 
 
ff9c083
63c7991
 
ff9c083
63c7991
 
 
ff9c083
 
 
 
 
 
 
 
 
 
63c7991
 
ff9c083
63c7991
 
 
 
 
 
ff9c083
 
 
 
 
 
 
 
 
 
 
 
 
 
63c7991
ff9c083
 
63c7991
ff9c083
 
 
 
 
63c7991
ff9c083
63c7991
 
ff9c083
 
 
 
63c7991
 
 
 
ff9c083
63c7991
ff9c083
63c7991
 
 
 
 
 
 
ff9c083
63c7991
 
 
ff9c083
63c7991
ff9c083
63c7991
 
 
 
 
 
 
ff9c083
 
 
 
 
 
 
 
 
63c7991
 
 
ff9c083
63c7991
ff9c083
63c7991
 
 
ff9c083
 
 
 
 
 
 
 
 
 
 
 
 
63c7991
 
 
 
 
ff9c083
 
63c7991
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
"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 { AnimatePresence, motion } from "framer-motion";
import { FormEvent, useEffect, useState } from "react";
import LoadingDots from "../../components/loading-dots";
import * as Select from "@radix-ui/react-select";

function removeCodeFormatting(code: string): string {
  return code.replace(/```(?:typescript|javascript|tsx)?\n([\s\S]*?)```/g, "$1").trim();
}

export default function Home() {
  const [status, setStatus] = useState<"initial" | "creating" | "created">("initial");
  const [prompt, setPrompt] = useState("");
  const [model, setModel] = useState("gemini-2.0-flash-exp");
  const [generatedCode, setGeneratedCode] = useState("");
  const [ref, scrollTo] = useScrollTo();

  const 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" },
  ];

  const loading = status === "creating";

  async function createApp(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("creating");
    setGeneratedCode("");

    try {
      const res = await fetch("/api/generateCode", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          model,
          messages: [{ role: "user", content: prompt }],
        }),
      });

      if (!res.ok) {
        const errorBody = await res.text();
        throw new Error(`HTTP Error ${res.status}: ${res.statusText}. ${errorBody}`);
      }

      const reader = res.body?.getReader();
      if (!reader) throw new Error("The response does not contain a body.");

      let receivedData = "";
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        receivedData += new TextDecoder().decode(value);
      }

      const cleanedData = removeCodeFormatting(receivedData);
      setGeneratedCode(cleanedData);
      setStatus("created");
    } catch (error: any) {
      console.error("Error during code generation:", error.message);
      setStatus("initial");
    }
  }

  useEffect(() => {
    const el = document.querySelector(".cm-scroller");
    if (el && loading) {
      const 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 items-center rounded-3xl bg-gray-300/50 px-7 py-5 shadow-sm dark:bg-gray-800/50"
        href="https://ai.google.dev/gemini-api/docs"
        target="_blank"
      >
        Powered by <span className="font-medium">Gemini API</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="relative flex bg-white shadow-md rounded-xl">
              <textarea
                rows={3}
                required
                value={prompt}
                onChange={(e) => setPrompt(e.target.value)}
                className="w-full resize-none rounded-l-xl p-4"
                placeholder="Build me a calculator app..."
              />
              <button
                type="submit"
                disabled={loading}
                className="bg-blue-500 text-white px-5 rounded-r-xl"
              >
                {status === "creating" ? <LoadingDots /> : <ArrowLongRightIcon />}
              </button>
            </div>
          </div>

          <div className="mt-6 flex items-center gap-4">
            <label className="text-gray-500">Model:</label>
            <Select.Root value={model} onValueChange={(value) => setModel(value)}>
              <Select.Trigger className="p-2 bg-gray-100 rounded-md">{model}</Select.Trigger>
              <Select.Content>
                {models.map((model) => (
                  <Select.Item key={model.value} value={model.value}>
                    {model.label}
                  </Select.Item>
                ))}
              </Select.Content>
            </Select.Root>
          </div>
        </fieldset>
      </form>

      {status !== "initial" && (
        <motion.div initial={{ height: 0 }} animate={{ height: "auto" }} className="w-full">
          <CodeViewer code={generatedCode} />
        </motion.div>
      )}
    </main>
  );
}