Introduction
Struggling with the daunting task of finding precise code solutions, I created CodeGPT. This AI-powered code generator swiftly converts natural language descriptions into code snippets, alleviating the frustration of tedious searches. Say goodbye to coding woes and embrace effortless solutions with CodeGPT.
We'll cover how to:
build web applications with Next.js,
langchain, Gemini for generative AI capabilities,
integrate AI into software applications with CopilotKit.
Prerequisites
Before diving into this tutorial, make sure you have the following:
- Basic understanding of programming.
- Familiarity with Next.js, TypeScript, and Tailwind CSS.
- Comfortable using command line for npm and Git.
- Node.js and npm installed.
Project Set up and Package Installation
To kickstart your project, let's begin by creating a Next.js application. Open your terminal and run the following command:
npx create-next-app codeGPT
Finally, let's install the CopilotKit packages. These packages enable us to interact with the React state and integrate an AI copilot into our application:
npm install @copilotkit/react-ui @copilotkit/react-textarea @copilotkit/react-core @copilotkit/backend
For authentication, you can use Auth.js with Drizzle ORM to store message.
For database, you can use tuso.tech
Building CodeGPT
In this section, I'll walk through the api endpoint development with the chat ui.
File structure
|-src
----|-app
------|-api
--------|-auth
----------|-[[...nextauth]]
------------|-route.tsx
--------|-copilotkit
----------|-route.ts
------|-layout.tsx
------|-page.tsx
------|-action.ts
------|-chat
--------|-[id]
----------|-page.tsx
--------|-new-chat.tsx
----|-components
------|-ui
------|-chat-content.tsx
------|-chat-list.tsx
------|-growing-text-area.tsx
------|-NavBar.tsx
----|-db
------|-index.ts
------|-schema.ts
----|-lib
------|-utils.ts
----|-auth.ts
|-drizzle.config.ts
|-.env
API Endpoint
src/app/api/copilotkit/route.ts
POST /route
This endpoint is part of the Copilot Kit and uses Google's Generative AI to generate responses based on the provided input.
Request
The request body should contain a forwardedProps object. If forwardedProps.choices is undefined, it will be set to an empty array. If forwardedProps.choices is not an array, an error will be returned.
Response
The endpoint responds with a string generated by Google's Generative AI. The AI is given a prompt that describes the expected behavior of the AI, followed by the choices provided in forwardedProps.choices.
If an error occurs during the generation of the response, the string "An error occurred" is returned. If forwardedProps.choices is not an array, the string "Invalid input: forwardedProps.choices is not an array" is returned.
import {
CopilotRuntime,
LangChainAdapter,
LangChainReturnType,
} from "@copilotkit/backend";
import { GoogleGenerativeAI } from "@google/generative-ai";
import { StreamingTextResponse } from "ai";
const genAI = new GoogleGenerativeAI(process.env.GENERATIVE_AI_API_KEY!);
export async function POST(req: Request) {
const copilotKit = new CopilotRuntime();
return copilotKit.response(
req,
new LangChainAdapter(
async (forwardedProps): Promise<LangChainReturnType> => {
console.log("forwardedProps", forwardedProps);
// If forwardedProps.choices is undefined, set it to an empty array
if (forwardedProps.choices === undefined) {
forwardedProps.choices = [];
}
// Check if forwardedProps.choices is an array
if (Array.isArray(forwardedProps.choices)) {
// Transform forwardedProps into the expected format
const prompt =
`As an expert in software development, you excel in utilizing the latest technologies and methodologies across all programming languages and frameworks. Your expertise extends to creating visually appealing and functionally robust UI designs. Your responses are exclusively in code form, focusing on delivering comprehensive, executable solutions without explanatory text. Your approach emphasizes code clarity, adherence to best practices, and the use of cutting-edge tools. You meticulously follow specified requirements regarding libraries and languages, ensuring your code contributions are fully integrated and operational within the given context. Your primary objective is to communicate solely through code, providing complete and self-sufficient code responses that align with the user's directives.` +
`I am in the process of enhancing a pre-existing application with new functionalities and improvements. Your assistance is sought for introducing new features and refining the current codebase to elevate its readability and adherence to contemporary best practices. The expectation is for the solutions to embody the latest advancements in software development, tailored to the specified technologies and frameworks. Your contributions should be comprehensive, avoiding partial or differential code snippets, and should be ready to run or compile as provided. It is imperative that your code aligns with the project's existing language, style, and libraries, unless a conversion or transformation is requested. Your responses should be purely code-based, fulfilling the requirement to communicate exclusively through well-structured and complete code examples.`;
try {
const modelInstance = genAI.getGenerativeModel({
model: "gemini-pro",
});
const chatCompletion = await modelInstance.generateContent([
prompt + forwardedProps.choices.join(" "),
]);
const responseStream = new ReadableStream({
start(controller) {
// Convert the string to a Uint8Array
const uint8Array = new TextEncoder().encode(
chatCompletion.response.text()
);
controller.enqueue(uint8Array);
controller.close();
},
});
const streaming_to_string = new StreamingTextResponse(
responseStream
);
return streaming_to_string.text();
} catch (error) {
// console.error(error);
return "An error occurred";
}
} else {
console.error("forwardedProps.choices is not an array");
// Handle the error appropriately...
// For example, you might want to return an error message:
return "Invalid input: forwardedProps.choices is not an array";
}
}
)
);
}
ChatListPage Component
src/app/chat/[id]/page.tsx
The ChatListPage is a React component that displays a list of chat messages for a specific user.
Props
The component accepts a single prop:
params: An object that contains a single property id, which is the ID of the user whose messages should be displayed.
Functionality
The component first checks if the user is authenticated by calling the auth() function. If the user is not authenticated, it returns a div with the text "Not logged in".
If the user is authenticated, it queries the usersMessages table in the database for messages that match the provided user ID. This is done using the db.select().from().where() function chain from the drizzle-orm library.
The component then returns a div that maps over the chat array and returns a ChatContent component for each message. The ChatContent component is passed two props: session (the current user session) and content (the content of the message).
import { auth } from "@/auth";
import ChatContent from "@/components/chat-content";
import { db } from "@/db";
import { usersMessages } from "@/db/schema";
import { eq } from "drizzle-orm";
import React from "react";
const ChatListPage = async ({ params }: { params: { id: string } }) => {
const session = await auth();
if (!session) {
return (
<div className="font-bold dark:text-white text-black">Not logged in</div>
);
}
const chat = await db
.select()
.from(usersMessages)
.where(eq(usersMessages.id, params.id));
return (
<div>
{chat.map((chat) => {
return (
<div className="flex flex-col gap-1" key={chat.id}>
<ChatContent session={session} content={chat.message} />
</div>
);
})}
</div>
);
};
export default ChatListPage;
NewChat Component
The NewChat is a React component that provides a button for creating a new chat.
Functionality
When the "New Chat" button is clicked, the user is redirected to the root ("/") route of the application. This is done using the useRouter hook from Next.js, which provides access to the router object. The router.push("/") function is called when the button is clicked, which navigates to the root route.
"use client";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import React from "react";
const NewChat = () => {
const router = useRouter();
return <Button onClick={() => router.push("/")}>New Chat</Button>;
};
export default NewChat;
action.ts
src/app/action.ts
This file contains two functions, storeMessage and Chatlist, which interact with a database to store and retrieve chat messages.
storeMessage(user_id: string, content: string)
This function takes a user_id and content as parameters and stores a new message in the usersMessages table in the database.
First, it checks if a user with the given user_id exists in the users table. If the user does not exist, the function returns and does not store the message.
If the user does exist, the function attempts to insert a new row into the usersMessages table with the userId, message, createdAt, and id fields. If an error occurs during this process, it is logged to the console and an Error is thrown.
Chatlist(user_id: string)
This function takes a user_id as a parameter and retrieves all messages from the usersMessages table in the database that match the given user_id.
The function returns an array of messages.
"use server";
import { db } from "@/db";
import { users, usersMessages } from "@/db/schema";
import { eq } from "drizzle-orm";
// store the message to the database
export async function storeMessage(user_id: string, content: string) {
const User = await db
.select({ users })
.from(users)
.where(eq(users.id, user_id));
if (!User) return;
try {
await db.insert(usersMessages).values({
userId: `${user_id}`,
message: content,
createdAt: new Date(),
id: crypto.randomUUID(),
});
} catch (error) {
console.error("Failed to store message: ", error);
throw new Error("Failed to store message");
}
}
export async function Chatlist(user_id: string) {
const chat = await db
.select()
.from(usersMessages)
.where(eq(usersMessages.userId, user_id));
return chat;
}
RootLayout and Page Component
src/layout.tsx
The RootLayout component is a wrapper component that provides global styles and context to all child components.
Props
The component accepts a single prop:
children: The child components to be rendered within the RootLayout.
Functionality
The RootLayout component sets up the global styles and context for the application. It uses the ThemeProvider component to provide a theme context to all child components. The NavBar component is rendered at the top of the page, and the CopilotKit component wraps the child components, providing them with access to the Copilot Kit's functionalities.
import ChatContent from "../components/chat-content";
import { auth } from "@/auth";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default async function Page() {
const session = await auth();
if (!session) {
return (
<main className="flex flex-col gap-6 items-center justify-center h-screen">
<div className="flex flex-col gap-4 items-center justify-center overflow-x-auto w-[80vh] text-center">
<h1 className="text-2xl lg:text-4xl w-[50vh] font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-green-500 to-blue-500">
Streamline Your Coding Experience with CodeGPT
</h1>
<p className="font-light w-[50vh]">
CodeGPT is the ultimate solution for developers looking to simplify
their coding workflow. Our AI-powered web app generates code
snippets instantly based on your natural language descriptions. With
CodeGPT, coding has never been easier.
</p>
<p className="text-md font-bold underline">Get Started Today!</p>
</div>
<Link href={"api/auth/signin"}>
<Button className=" text-center font-bold">Log in</Button>
</Link>
</main>
);
}
return (
<main className="max-h-screen">
<ChatContent session={session} />
</main>
);
}
src/page.tsx
The Page component is an asynchronous function that checks if the user is authenticated and renders different content based on the authentication status.
Functionality
The Page component first calls the auth function to check if the user is authenticated. If the user is not authenticated, it renders a landing page with a description of CodeGPT and a login button.
If the user is authenticated, it renders the ChatContent component, passing the user's session as a prop.
import ChatContent from "../components/chat-content";
import { auth } from "@/auth";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default async function Page() {
const session = await auth();
if (!session) {
return (
<main className="flex flex-col gap-6 items-center justify-center h-screen">
<div className="flex flex-col gap-4 items-center justify-center overflow-x-auto w-[80vh] text-center">
<h1 className="text-2xl lg:text-4xl w-[50vh] font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-green-500 to-blue-500">
Streamline Your Coding Experience with CodeGPT
</h1>
<p className="font-light w-[50vh]">
CodeGPT is the ultimate solution for developers looking to simplify
their coding workflow. Our AI-powered web app generates code
snippets instantly based on your natural language descriptions. With
CodeGPT, coding has never been easier.
</p>
<p className="text-md font-bold underline">Get Started Today!</p>
</div>
<Link href={"api/auth/signin"}>
<Button className=" text-center font-bold">Log in</Button>
</Link>
</main>
);
}
return (
<main className="max-h-screen">
<ChatContent session={session} />
</main>
);
}
ChatContent Component
src/component/chat-content.tsx
The ChatContent component is a React component that handles the chat functionality of the application.
Props
session: The user's session data.
content: Optional initial content for the chat.
Functionality
The component maintains the state of the assistant's response, loading status, and copy button text. It handles the submission of chat input, stopping the chat, and copying the assistant's response to the clipboard. It also stores the assistant's response in the database.
The component renders a chat interface, including the chat input and the assistant's response. The response is rendered as markdown, with syntax highlighting for code blocks. If there's no response yet, it displays a default message.
"use client";
import { useState, useRef } from "react";
import ChatInput from "@/components/chat-input";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus as dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { convertFileToBase64 } from "@/lib/utils";
import { Copy } from "lucide-react";
import { Button } from "./ui/button";
import { storeMessage } from "@/app/action";
type ChatContentProps = {
content?: string;
session: any;
};
export default function ChatContent({ session, content }: ChatContentProps) {
const [assisnantResponse, setAssistantResponse] = useState("");
const [isLoading, setIsLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const [copyButtonText, setCopyButtonText] = useState("Copy");
const handleSubmit = async (value: string, file?: File) => {
setIsLoading(true);
setAssistantResponse("");
let body = "";
if (file) {
const imageUrl = await convertFileToBase64(file);
const content = [
{
type: "image_url",
image_url: {
url: imageUrl,
},
},
{
type: "text",
text: value,
},
];
body = JSON.stringify({ content });
} else {
body = JSON.stringify({ content: value });
}
try {
abortControllerRef.current = new AbortController();
const res = await fetch("/api/copilotkit", {
method: "POST",
body: body,
headers: {
"Content-Type": "application/json",
},
signal: abortControllerRef.current.signal,
});
if (!res.ok || !res.body) {
alert("Error sending message");
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
const text = decoder.decode(value);
setAssistantResponse((currentValue) => currentValue + text);
if (done) {
break;
}
}
} catch (error: any) {
if (error.name !== "AbortError") {
alert("Error sending message");
}
}
abortControllerRef.current = null;
setIsLoading(false);
};
const handleStop = () => {
if (!abortControllerRef.current) {
return;
}
abortControllerRef.current.abort();
abortControllerRef.current = null;
};
const copyMarkdownToClipboard = () => {
try {
navigator.clipboard.writeText(assisnantResponse);
} catch (error) {
console.error("Failed to copy content: ", error);
}
};
const handleCopyClick = async () => {
await copyMarkdownToClipboard();
setCopyButtonText("Copied...");
setTimeout(() => setCopyButtonText("Copy"), 1000);
};
const userId = session.user.id;
if (assisnantResponse.length > 0) {
storeMessage(`${userId}`, assisnantResponse);
}
return (
<div className="flex flex-col h-screen">
<div className="max-w-4xl w-full max-h-[70vh] mx-auto flex-1 px-10 py-5 overflow-x-hidden overflow-y-scroll custom-scrollbar prose dark:prose-invert">
{(content || assisnantResponse) && (
<div className="flex justify-end">
<Button onClick={handleCopyClick}>
<Copy size={24} />
<span className="ml-2">{copyButtonText}</span>
</Button>
</div>
)}
<Markdown
remarkPlugins={[remarkGfm]}
components={{
code(props) {
const { children, className, node, ...rest } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
PreTag="div"
language={match[1]}
style={dark}
wrapLines={true}
wrapLongLines={true}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code {...rest} className={className}>
<div className="overflow-y-auto">{children}</div>
</code>
);
},
}}
>
{assisnantResponse || content}
</Markdown>
{!assisnantResponse && !content && (
<div className="text-center text-gray-400 flex flex-col gap-4">
<span>Ask me anything related to Code-gen! 🚀 </span>
<span>Enjoy the experience! 🎉</span>
</div>
)}
</div>
<ChatInput
onSubmit={handleSubmit}
isStreaming={isLoading}
onStop={handleStop}
/>
</div>
);
}
ExpandingInput Component
src/component/chat-input.tsx
The ExpandingInput component is a React component that provides a text input field and an image selection field for user input. It also provides a submit button and a stop button for controlling the input submission. The component maintains the state of the input content and the selected image. The onSubmit and onStop callbacks are called when the submit and stop buttons are clicked, respectively.
"use client";
import { useState } from "react";
import GrowingTextArea from "./growing-text-area";
import { cn } from "@/lib/utils";
import ImageSelection from "./image-selection";
export default function ExpandingInput({
onSubmit,
onStop,
isStreaming,
}: {
onSubmit?: (value: string, file?: File) => void;
onStop?: () => void;
isStreaming?: boolean;
}) {
const [content, setContent] = useState("");
const [selectedImage, setSelectedImage] = useState<File | undefined>(
undefined
);
const submit = (value: string) => {
onSubmit?.(value, selectedImage);
setContent("");
setSelectedImage(undefined);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
submit(content);
};
const buttonDisabled = content.length === 0 || isStreaming;
return (
<div className="w-full">
<form
onSubmit={handleSubmit}
className="w-full flex flex-col gap-y-4 px-4 relative max-w-5xl mx-auto"
>
<ImageSelection
selectedImage={selectedImage}
setSelectedImage={setSelectedImage}
/>
<GrowingTextArea
className="w-full bg-transparent border border-gray-500 rounded-2xl outline-none resize-none pl-12 pr-14 py-4 scrollbar-content overflow-y-auto overflow-x-clip overscroll-contain"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
{isStreaming ? (
<button
type="button"
onClick={onStop}
className="flex absolute right-0 bottom-0 px-1 py-1 mr-7 mb-2 rounded-2xl z-10 w-10 h-10 items-center justify-center dark:fill-neutral-300 :fill-neutral-700 dark:hover:fill-neutral-100 hover:fill-neutral-900 transition-all"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="m12,0C5.383,0,0,5.383,0,12s5.383,12,12,12,12-5.383,12-12S18.617,0,12,0Zm0,22c-5.514,0-10-4.486-10-10S6.486,2,12,2s10,4.486,10,10-4.486,10-10,10Zm2-15h-4c-1.654,0-3,1.346-3,3v4c0,1.654,1.346,3,3,3h4c1.654,0,3-1.346,3-3v-4c0-1.654-1.346-3-3-3Zm1,7c0,.551-.449,1-1,1h-4c-.551,0-1-.449-1-1v-4c0-.551.449-1,1-1h4c.551,0,1,.449,1,1v4Z" />
</svg>
</button>
) : (
<button
className={cn(
"flex absolute right-0 bottom-0 px-1 py-1 mr-7 mb-2 dark:bg-white bg-black rounded-2xl z-10 w-10 h-10 items-center justify-center",
buttonDisabled && "opacity-50"
)}
disabled={buttonDisabled}
type="submit"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
className="text-white dark:text-black w-7 h-7"
>
<title>Submit</title>
<path
d="M7 11L12 6L17 11M12 18V7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
</button>
)}
</form>
</div>
);
}
ChatList Component
The ChatList component is a React component that displays a list of chat messages for a logged-in user. If the user is not logged in, it displays a login prompt. The chat messages are fetched from a database. Each message is a link that navigates to a detailed view of the chat.
import { auth } from "@/auth";
import Link from "next/link";
import { Button } from "./ui/button";
import { db } from "@/db";
import { usersMessages } from "@/db/schema";
import { eq } from "drizzle-orm";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "./ui/separator";
export const dynamic = "force-dynamic";
export default async function ChatList() {
const session = await auth();
if (!session) {
return (
<div className="flex flex-col gap-4 text-center">
<p className="font-bold">Not logged in</p>
<Link href="/api/auth/signin">
<Button>Login</Button>
</Link>
</div>
);
}
const chat = await db
.select()
.from(usersMessages)
.where(eq(usersMessages.userId, session.user?.id!));
return (
<ScrollArea className="h-[90vh] max-w-full rounded-md ">
<div className="p-4">
{chat.map((c) => (
<>
<Link href={`/chat/${c.id}`} className="flex flex-col gap-1">
<span className="text-sm overflow-hidden">{c.id}</span>
<span>{c.createdAt.toDateString()}</span>
</Link>
<Separator className="my-2" />
</>
))}
</div>
</ScrollArea>
);
}
src/db/schema.ts
import {
integer,
sqliteTable,
text,
primaryKey,
} from "drizzle-orm/sqlite-core";
import type { AdapterAccount } from "next-auth/adapters";
export const users = sqliteTable("user", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").notNull(),
emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
image: text("image"),
});
export const usersMessages = sqliteTable("userMessage", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
message: text("message").notNull(),
createdAt: integer("createdAt", { mode: "timestamp_ms" }).notNull(),
});
export type UserMessage = typeof usersMessages.$inferInsert;
export const accounts = sqliteTable(
"account",
{
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").$type<AdapterAccount["type"]>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("providerAccountId").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
},
(account) => ({
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId],
}),
})
);
export const sessions = sqliteTable("session", {
sessionToken: text("sessionToken").primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
});
export const verificationTokens = sqliteTable(
"verificationToken",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
},
(vt) => ({
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
})
);
.env
GENERATIVE_AI_API_KEY=
TURSO_CONNECTION_URL=
TURSO_AUTH_TOKEN=
AUTH_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
Conclusion
With the completion of this tutorial, you've laid the foundation for an innovative spreadsheet application powered by CodeGPT. By integrating AI capabilities with Next.js, TypeScript, and Tailwind CSS, you're poised to create a versatile and efficient tool for data management and analysis. Embrace the possibilities of AI-driven development and continue exploring the limitless potential of CodeGPT in your projects. Happy coding!
You can find the source code for this tutorial on GitHub:
rajanshresth / code-gpt
Code-GPT is an AI-powered web app that swiftly generates code snippets based on user descriptions, revolutionizing the coding experience with its intuitive interface and versatile language support.
CodeGPT
CodeGPT is a revolutionary web application powered by AI, designed to streamline the coding experience. Generate code snippets swiftly and effortlessly with our intuitive interface and versatile language support.
Project URL
: CodeGPT Live
<iframe width="560" height="315" src="https://youtu.be/AWi7S624vCU" frameborder="0" allowfullscreen></iframe>
Getting Started
To get started with CodeGPT locally, follow these steps:
- Clone this repository:
git clone https://github.com/rajanshresth/code-gpt.git
- Navigate to the project directory:
cd code-gpt
- Install dependencies:
npm install
# or
yarn
# or
pnpm install
- Run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
Open http://localhost:3000 with your browser to see the result.
Tech Stack
- Next.js: Next.js powers the frontend of CodeGPT, providing a fast and efficient web development experience.
- Gemini: We leverage Gemini for generative AI capabilities, enhancing CodeGPT's code generation capabilities.
- TypeScript: TypeScript brings static typing to JavaScript, improving code quality and developer productivity.
- Tailwind…
Top comments (7)
Nice❤️
Nice concept
I've used the project and I've successfully generated some code.
good project ❤️🫡
Nice❤️
great one❤👍
Not Bad 😀