diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..bffb357a7122523ec94045523758c4b825b448ef --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..ebbbed185c71b3c43e01efb23339cfd2004bc725 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +public/Aeonik/Aeonik-Bold.ttf filter=lfs diff=lfs merge=lfs -text +public/halo.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..00bba9bb2902b0af9ef944b4dc1b99ed5f65a497 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..b4bfed3579c4afed352be3bdd29abdb42c784e50 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..9882e628435f2d186a49637c31b9d9a3bfb79745 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hassan El Mghari + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8f359581e7beb65ef14e2be37d748c80aa2b2cfe --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +

InstantCoder

+

+ Generate small apps with one prompt. Powered by the Gemini API. +

+ +Try it in https://huggingface.co/spaces/osanseviero/InstantCoder + +This project is fully based on [llamacoder](https://github.com/Nutlope/llamacoder). Please follow [Nutlope](https://github.com/Nutlope) and give them a star. + +## Tech stack + +- [Gemini API](https://ai.google.dev/gemini-api/docs) to use Gemini 1.5 Pro, Gemini 1.5 Flash, and Gemini 2.0 Flash Experimental +- [Sandpack](https://sandpack.codesandbox.io/) for the code sandbox +- Next.js app router with Tailwind + +You can also experiment with Gemini in [Google AI Studio](https://aistudio.google.com/). + +## Cloning & running + +1. Clone the repo: `git clone https://github.com/osanseviero/GemCoder` +2. Create a `.env` file and add your [Google AI Studio API key](https://aistudio.google.com/app/apikey): `GOOGLE_AI_API_KEY=` +3. Run `npm install` and `npm run dev` to install dependencies and run locally + +**This is a personal project and not a Google official project** diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a99f04ec4bf035c6c170125981bdc16185d31c3c --- /dev/null +++ b/app/(main)/layout.tsx @@ -0,0 +1,36 @@ +import Image from "next/image"; +import bgImg from "@/public/halo.png"; +import Footer from "@/components/Footer"; +import Header from "@/components/Header"; +import ThemeToggle from "@/components/ThemeToggle"; + +export default function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + +
+
+ +
+ +
+
+
+ +
+
+ {children} +
+
+
+ + ); +} diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..15bb0f0e84685eaefcc8f353de89a97f65c7dd2f --- /dev/null +++ b/app/(main)/page.tsx @@ -0,0 +1,250 @@ +"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 [modification, setModification] = useState(""); + let [generatedCode, setGeneratedCode] = useState(""); + let [initialAppConfig, setInitialAppConfig] = useState({ + model: "", + }); + let [ref, scrollTo] = useScrollTo(); + let [messages, setMessages] = useState<{ role: string; content: string }[]>( + [], + ); + + let loading = status === "creating" || status === "updating"; + + async function createApp(e: FormEvent) { + 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, + 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 }); + 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 ( +
+ + + Powered by Gemini API + + +

+ Turn your idea +
into an app +

+ +
+
+
+
+
+
+