DEV Community

Cover image for How to build: a v0.dev clone (Next.js, GPT4 & CopilotKit)
Ekemini Samuel for CopilotKit

Posted on • Updated on

How to build: a v0.dev clone (Next.js, GPT4 & CopilotKit)

TL;DR

In this article, you will learn how to build a clone of Vercel's V0.dev. This is an awesome project to add to your portfolio and to hone in your AI chops.

We will cover using:

  • Next.js for the app framework 🖥️
  • OpenAI for the LLM 🧠
  • App logic of v0 👾
  • Using CopilotKit to integrate the AI into your app 🪁

Image description


CopilotKit: the OS framework for in-app AI

CopilotKit is the open-source AI copilot platform. We make it easy to integrate powerful AI into your react apps.

Build:

  • ChatBot: Context aware in-app chatbots that can take actions in-app 💬

  • CopilotTextArea: AI-powered textFields with context-aware autocomplete & insertions 📝

  • Co-Agents: In-app AI agents that can interact with your app & users 🤖

Image description

Star CopilotKit ⭐️
(forgive the AI's spelling mistake & give a star :)

Now back to the article.


Prerequisites

To get started with this tutorial, you need the following:

  • A text editor (VS Code, Cursor)
  • Basic knowledge of React, Next.js, Typescript, and Tailwind CSS.
  • Node.js installed on your PC/Mac
  • A package manager (npm)
  • OpenAI API key
  • CopilotKit installed in your React project

What is v0?

v0 is a Generative user interface (UI) tool developed by Vercel that allows users to give prompts and describe their ideas which are translated into UI code for creating web interfaces. It utilizes generative AI, along with open-source tools such as React, Tailwind CSS, and Shadcn UI, to produce code based on descriptions provided by the user.

Here is an example of a web app UI generated with v0
https://v0.dev/t/nxGnMd1uVGc

Understanding the Project Requirements

At the end of this step-by-step tutorial, the clone will have these project requirements:

  1. User Input: Users input text as prompts describing the UI they want to generate. This will be done using the CopilotKit chatbot, made available by the CopilotSidebar.
  2. CopilotKit Integration: CopilotKit will be used to give AI functionality to the web app for generating UIs.
  3. Rendering the UI: A toggle to switch between the UI React/JSX code and rendered UI.

Creating the v0 clone with CopilotKit

Step 1: Create a new Next.JS app
Open your workspace folder in your terminal and run the following command create a new Next.js app:

npx create-next-app@latest copilotkit-v0-clone
Enter fullscreen mode Exit fullscreen mode

This will create a new directory named copilotkit-v0-clone with a Next.JS project structure, with the required dependencies installed. It will show this in your terminal, choose Yes for all of them except the last one, as the default import alias is recommended. The other prompts install Typescript and TailwindCSS which we will use in the project.

terminal

Navigate to the project directory using the cd command like so:

cd copilotkit-v0-clone
Enter fullscreen mode Exit fullscreen mode

Step 2: Setup CopilotKit Backend Endpoint. Read the docs to learn more.

Run this command to install the CopilotKit backend packages:

npm i @copilotkit/backend
Enter fullscreen mode Exit fullscreen mode

Then visit https://platform.openai.com/api-keys to get your GPT 4 OpenAI API key.

openai

Once you have your API key, create a .env.local file in the root directory.
The .env.local file should be like this:

OPENAI_API_KEY=Your OpenAI API key
Enter fullscreen mode Exit fullscreen mode

In the app directory create this directory; api/copilot/openai and create a file named route.ts This file serves as the backend endpoint for CopilotKit requests and OpenAI interactions. It handles incoming requests, processes them using CopilotKit, and returns the appropriate response.

We will create a POST request function in the route.ts file, inside the post request create a new instance of CopilotBackend class, this class provides methods for processing CopilotKit requests.
We then call the response method of the CopilotBackend instance, passing the request object (req) and a new instance of the OpenAIAdapter class as arguments. This method processes the request using CopilotKit and the OpenAI API and returns a response.

As shown in the code below, we import the CopilotBackend and OpenAIAdapter classes from the @copilotkit/backend package. These classes are necessary for interacting with CopilotKit and the OpenAI API.

import { CopilotBackend, OpenAIAdapter } from "@copilotkit/backend";

export const runtime = "edge";

export async function POST(req: Request): Promise<Response> {
  const copilotKit = new CopilotBackend();

  return copilotKit.response(req, new OpenAIAdapter());
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Creating Components for the v0 clone
We will be using components from Shadcn UI library. To process let’s setup Shadcn UI library by running the shadcn-ui init command to setup your project

npx shadcn-ui@latest init
Enter fullscreen mode Exit fullscreen mode

Then we will configure the components.json with this questions

Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Do you want to use CSS variables for colors? › no / yes
Enter fullscreen mode Exit fullscreen mode

The components we are using from Shadcn UI are button and dialog. So let’s install them!
For the button, run this command

npx shadcn-ui@latest add button
Enter fullscreen mode Exit fullscreen mode

To install the dialog component run the command below

npx shadcn-ui@latest add dialog
Enter fullscreen mode Exit fullscreen mode

Step 4: Setup CopilotKit Frontend. Read the docs to learn more.
To install the CopilotKit frontend packages run this command:

npm i @copilotkit/react-core @copilotkit/react-ui 
Enter fullscreen mode Exit fullscreen mode

From the CopilotKit documentation, to use CopilotKit, we must set up the frontend wrapper to pass any React app through Copilot. When the prompt is passed to CopilotKit, it sends it through the URL to OpenAI which returns the response.

In the app directory, let’s update the layout.tsx file. This file will define the layout structure of our application and integrate CopilotKit into the frontend.

Enter the code below:

"use client";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-textarea/styles.css"; // also import this if you want to use the CopilotTextarea component
import "@copilotkit/react-ui/styles.css"; 
import { Inter } from "next/font/google";
import "./globals.css";
import { CopilotSidebar,  } from "@copilotkit/react-ui";

const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <CopilotKit url="/api/copilotkit/openai/">
          <CopilotSidebar defaultOpen>{children}</CopilotSidebar>
        </CopilotKit>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

This component represents the root layout of our application. It wraps the entire application with CopilotKit, specifying the URL for CopilotKit's backend endpoint (/api/copilotkit/openai/), based on what we created in the Step 2 for the backend. Additionally, it includes a CopilotSidebar component, acting as a sidebar for CopilotKit, with the children prop passed as its content.

Step 5: Setting up the Main App

Let’s create the structure of the application. It will have a Header, Sidebar and Preview screen.

For the Header, navigate to the components directory like this, src/components then create a header.tsx file and enter the code below:

import { CodeXmlIcon } from "lucide-react";
import { Button } from "./ui/button";

const Header = (props: { openCode: () => void }) => {
    return (
      <div className="w-full h-20 bg-white flex justify-between items-center px-4">
        <h1 className="text-xl font-bold">Copilot Kit</h1>
        <div className="flex gap-x-2">
          <Button
            className="  px-6 py-1 rounded-md space-x-1"
            variant={"default"}
            onClick={props.openCode}
          >
            <span>Code</span> <CodeXmlIcon size={20} />
          </Button>
        </div>
      </div>
    );
  };

  export default Header;
Enter fullscreen mode Exit fullscreen mode

For Sidebar create a sidebar.tsx file and enter this code:

import { ReactNode } from "react";

const Sidebar = ({ children }: { children: ReactNode }) => {
  return (
    <div className="w-[12%] min-h-full bg-white rounded-md p-4">
      <h1 className="text-sm mb-1">History</h1>
      {children}
    </div>
  );
};

export default Sidebar;

Enter fullscreen mode Exit fullscreen mode

Then for the Preview screen, create a preview-screen.tsx file and enter the code:

const PreviewScreen = ({ html_code }: { html_code: string }) => {
    return (
      <div className="w-full h-full bg-white rounded-lg  shadow-lg p-2 border">
        <div dangerouslySetInnerHTML={{ __html: html_code }} />
      </div>
    );
  };
  export default PreviewScreen; 
Enter fullscreen mode Exit fullscreen mode

Now let’s bring them together, open the page.tsx file and paste the following code:

"use client";
import { useState } from "react";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import Header from "@/components/header";
import Sidebar from "@/components/sidebar";
import PreviewScreen from "@/components/preview-screen";
import { Input } from "@/components/ui/input";

export default function Home() {
  const [code, setCode] = useState<string[]>([
    `<h1 class="text-red-500">Hello World</h1>`,
  ]);
  const [codeToDisplay, setCodeToDisplay] = useState<string>(code[0] || "");
  const [showDialog, setShowDialog] = useState<boolean>(false);
  const [codeCommand, setCodeCommand] = useState<string>("");

  return (
    <>
      <main className="bg-white min-h-screen px-4">
        <Header openCode={() => setShowDialog(true)} />
        <div className="w-full h-full min-h-[70vh] flex justify-between gap-x-1 ">
          <Sidebar>
            <div className="space-y-2">
              {code.map((c, i) => (
                <div
                  key={i}
                  className="w-full h-20 p-1 rounded-md bg-white border border-blue-600"
                  onClick={() => setCodeToDisplay(c)}
                >
                  v{i}
                </div>
              ))}
            </div>
          </Sidebar>

          <div className="w-10/12">
            <PreviewScreen html_code={codeToDisplay || ""} />
          </div>
        </div>
        <div className="w-8/12 mx-auto p-1 rounded-full bg-primary flex my-4 outline-0">
          <Input
            type="text"
            placeholder="Enter your code command"
            className="w-10/12 p-6 rounded-l-full  outline-0 bg-primary text-white"
            value={codeCommand}
            onChange={(e) => setCodeCommand(e.target.value)}
          />
          <button
            className="w-2/12 bg-white text-primary rounded-r-full"
            onClick={() => generateCode.run(context)}
          >
            Generate
          </button>
        </div>
      </main>
      <Dialog open={showDialog} onOpenChange={setShowDialog}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>View Code.</DialogTitle>
            <DialogDescription>
              You can use the following code to start integrating into your
              application.
            </DialogDescription>
            <div className="p-4 rounded bg-primary text-white my-2">
              {readableCode}
            </div>
          </DialogHeader>
        </DialogContent>
      </Dialog>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let’s breakdown the code above:

const [code, setCode] = useState<string[]>([]); will be used to hold the generated code

const [codeToDisplay, setCodeToDisplay] = useState<string>(code[0] || ""); will be used to hold the code that is displayed on the Preview Screen.

const [showDialog, setShowDialog] = useState<boolean>(false); this will hold the state of the dialog box that shows the generated code you can copy.

In the code below, we loop over the generated code, which is a string of arrays, to show it on the Sidebar, so that when we select one, it is displayed on the Preview screen.

 <Sidebar>
            <div className="space-y-2">
              {code.map((c, i) => (
                <div
                  key={i}
                  className="w-full h-20 p-1 rounded-md bg-white border border-blue-600"
                  onClick={() => setCodeToDisplay(c)}
                >
                  v{i}
                </div>
              ))}
            </div>
          </Sidebar>
Enter fullscreen mode Exit fullscreen mode

<PreviewScreen html_code={codeToDisplay} /> here, we send the code to be displayed on the preview screen. The preview screen component takes the string of code generated by CopilotKit and uses dangerouslySetInnerHTML to render the generated code.

Below we have a Dialog component that will display the code generated by CoplilotKit that can be copied and added to your code.

<Dialog open={showDialog} onOpenChange={setShowDialog}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>View Code.</DialogTitle>
            <DialogDescription>
              You can use the following code to start integrating into your
              application.
            </DialogDescription>
            <div className="p-4 rounded bg-primary text-white my-2">
              {readableCode}
            </div>
          </DialogHeader>
        </DialogContent>
      </Dialog>
Enter fullscreen mode Exit fullscreen mode

Step 6: Implementing the Main Application Logic
For this step, we'll integrate CopilotKit into our v0 clone application to facilitate AI-powered UI generation. We'll use CopilotKit's React hooks to manage state, make components readable and actionable by Copilot, and interact with the OpenAI API.

On your page.tsx file, import this:

import {
  CopilotTask,

  useCopilotContext,
  useMakeCopilotReadable,
} from "@copilotkit/react-core";
Enter fullscreen mode Exit fullscreen mode

Then we define a generateCode task using CopilotTask in the Home component:


 const readableCode = useMakeCopilotReadable(codeToDisplay);

 const generateCode = new CopilotTask({
    instructions: codeCommand,
    actions: [
      {
        name: "generateCode",
        description: "Create Code Snippet with React.js, tailwindcss.",
        parameters: [
          {
            name: "code",
            type: "string",
            description: "Code to be generated",
            required: true,
          },
        ],
        handler: async ({ code }) => {
          setCode((prev) => [...prev, code]);
          setCodeToDisplay(code);
        },
      },
    ],
  });

  const context = useCopilotContext();
Enter fullscreen mode Exit fullscreen mode

We use useMakeCopilotReadable, to pass the existing code and ensure readability. Then we use CopilotTask to generate the UI and bind the generateCode task to the generate button, which enables the generation of code snippets by interacting with the button component.
This action is triggered by user interactions and executes an asynchronous handler function when invoked.

The handler adds the code generated to the code array, updates the application state to include the newly generated code snippet and also sends the generated code to be displayed and rendered on the Preview screen, which is available to be copied too.
Also, the instructions attribute specifies the command provided to Copilot, which is stored in the codeCommand state variable.

For a full description of how CopilotTask works, check the documentation here: https://docs.copilotkit.ai/reference/CopilotTask

Step 7: Run the v0 Clone Application

At this point, we have completed the v0 clone setup and can then start the development server by running

npm run dev
Enter fullscreen mode Exit fullscreen mode

terminal

The web app can be accessed in your browser with this URL

http://localhost:3000

You can then enter a prompt and click on Generate. Here are some examples:

  • Pricing page: As shown below, here is the generated UI, with a toggle button to switch between the UI and React code:

vo

If you click on the Code </> button on the top right, it switches to the React code for the UI generated like so:

vo

  • A Signup page UI example:

signup

  • And also a Checkout page

checkout

To clone the project and run it locally, open your terminal and run this command:

git clone https://github.com/Tabintel/v0-copilot-next
Enter fullscreen mode Exit fullscreen mode

Then run npm install to install all dependencies needed for the project and npm run dev to run the web app.

Conclusion

In conclusion, you can use CopilotKit to build a v0 clone to give UI prompts for your design. CopilotKit goes beyond UI prompts, it can also be used in building applications like AI-powered PowerPoint generators, AI resume builders, and many more.
The possibilities are endless, check out CopilotKit today and bring your AI ideas to life.
Get the full source code on GitHub.
Learn more about how to use CopilotKit from the documentation.

Also, don't forget to Star CopilotKit!

Top comments (40)

Collapse
 
srbhr profile image
Saurabh Rai

Wow this is really cool! Now we have v1!!

Collapse
 
envitab profile image
Ekemini Samuel

Thank you, Saurabh! Yes, next is v2 😎

Collapse
 
nevodavid profile image
Nevo David

This is really cool!

Collapse
 
envitab profile image
Ekemini Samuel

Thank you :)

Collapse
 
uliyahoo profile image
uliyahoo

Wow, awesome work!

Collapse
 
envitab profile image
Ekemini Samuel

Thank youu!

Collapse
 
saikiran76 profile image
Korada Saikiran

Interesting tech stack and really cool 🤠

Collapse
 
envitab profile image
Ekemini Samuel

Thank you Korada

Collapse
 
resiwicaksono98 profile image
Resi Wicaksono

hmmmm

Collapse
 
zack-123 profile image
Zack

If this actually works then it's cool...

Collapse
 
envitab profile image
Ekemini Samuel

Yep, it works.

Collapse
 
androaddict profile image
androaddict

Worth to read . Expecting more advance

Collapse
 
envitab profile image
Ekemini Samuel

Thank you, yes more implementations of CopilotKit are coming.

Collapse
 
john-123 profile image
John

v0 as in Vercel's v0?

Collapse
 
envitab profile image
Ekemini Samuel

Yes, Vercel's v0

Collapse
 
thomas123 profile image
Thomas

Nice

Collapse
 
envitab profile image
Ekemini Samuel

Thank you!

Collapse
 
shuttle_dev profile image
Shuttle

Awesome work!

Collapse
 
envitab profile image
Ekemini Samuel

Thank you team Shuttle!

Collapse
 
fernandezbaptiste profile image
Bap

Really really cool! 😎

Collapse
 
envitab profile image
Ekemini Samuel

Thank you :)

Collapse
 
william4411 profile image
william4411

This looks like a cool walkthrough. Excited to try it out over the weekend.

Collapse
 
envitab profile image
Ekemini Samuel

Thank you, William!

Collapse
 
ricardogesteves profile image
Ricardo Esteves • Edited

Niceee, Looks good!! Good job 👌

Collapse
 
envitab profile image
Ekemini Samuel

Thank you, Ricardo.

Collapse
 
the_greatbonnie profile image
Bonnie

Great work, Samuel.

Collapse
 
envitab profile image
Ekemini Samuel

Thank you, Bonnie.

Collapse
 
andrew0123 profile image
Andrew

Didn't think it would be this easy to clone v0!

Collapse
 
envitab profile image
Ekemini Samuel

It took some work though :)

Collapse
 
perriekkola profile image
Per • Edited

A really cool concept, but unfortunately the tutorial code and the repository shared doesn't work as expected. The HTML preview returns a string like "quMXj6KfFLs7gqzeX_UMJ" instead of working HTML/CSS.

The history sidebar doesn't match the one in your own screenshots, so it gets a bit confusing.

If possible, I would suggest that you run through your guide yourself and see if you run into the same problems, since I noticed another user had the same issues as me. I have double-checked the API key as well, no issues there.

Collapse
 
envitab profile image
Ekemini Samuel

Thank you. Please ensure that you run npm install to install all dependencies. Here is the repository:
github.com/Tabintel/v0-copilot-next

Collapse
 
baucucu profile image
Alex Raduca

there is an issue with readableCode defined on line 29 in page.tsx. it returns a string that looks like a unique ID.
I had to replace readableCode with codeToDisplay in JSX to make it work

Thread Thread
 
envitab profile image
Ekemini Samuel

Thank you, Alex. I have updated it with the changes from my local environment. It's now in sync.

Thread Thread
 
baucucu profile image
Alex Raduca

This is awsome! I forked it and I'm working on some minor improvements.
I managed to get a complete component made with gpt 4 turbo but it's not being displayed correctly. The code works however, I just copied it into a component file and rendered it.

Here is the generated code

import React from "react";
import { useState } from "react";
const LoginForm = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
console.log({ email, password, rememberMe });
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
{" "}
<form
className="p-6 bg-white shadow-md rounded-lg"
onSubmit={handleSubmit}
>
{" "}
<h2 className="text-2xl font-semibold text-gray-800 mb-4">
Login
</h2>{" "}
<div className="mb-4">
{" "}
<label
htmlFor="email"
className="block text-gray-700 text-sm font-bold mb-2"
>
Email Address
</label>{" "}
<input
type="email"
id="email"
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>{" "}
</div>{" "}
<div className="mb-4">
{" "}
<label
htmlFor="password"
className="block text-gray-700 text-sm font-bold mb-2"
>
Password
</label>{" "}
<input
type="password"
id="password"
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>{" "}
</div>{" "}
<div className="mb-4 flex items-center">
{" "}
<input
type="checkbox"
id="rememberMe"
className="mr-2"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>{" "}
<label htmlFor="rememberMe" className="text-sm text-gray-700">
Remember Me
</label>{" "}
</div>{" "}
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Login
</button>{" "}
<div className="mt-4 text-center">
{" "}
<a href="#" className="text-sm text-blue-500 hover:text-blue-700">
Forgot Password?
</a>{" "}
<br />{" "}
<a href="#" className="text-sm text-blue-500 hover:text-blue-700">
Register
</a>{" "}
</div>{" "}
</form>{" "}
</div>
);
};
export default LoginForm;

Image description