How to build: a To-Do list app with an embedded AI copilot (Next.js, GPT4, & CopilotKit)
Tags: webdev, programming, javascript, tutorial
Published: 2024-05-29 12:22:46

## **TL;DR**

A to-do list is a classic project for every dev. In today's world it is great to learn how to build with AI and to have some AI projects in your portfolio. 

Today, I will go through step by step of how to build a to-do list with an embedded AI copilot for some AI magic 🪄. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nw0jituk3y5tosz6f34u.gif)



We'll cover how to:

- Build the to-do list generator web app using Next.js, TypeScript, and Tailwind CSS.
- Use CopilotKit to integrate AI functionalities into the to-do list generator.
- Use AI chatbot to add lists, assign lists to someone, mark lists as completed, and delete lists.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/guf0l2fiq1g1jv86o7bg.png)



---

## CopilotKit: The framework for building in-app AI copilots



CopilotKit is an [open-source AI copilot framework](https://github.com/CopilotKit/CopilotKit). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/myp7zashy99m33ya8kaf.gif)



{% cta https://go.copilotkit.ai/bonnie %} Star CopilotKit ⭐️ {% endcta %}


---

## Prerequisites

To fully understand this tutorial, you need to have a basic understanding of React or Next.js.

Here are the tools required to build the AI-powered to-do list generator:

- [Nanoid](https://github.com/ai/nanoid) - a tiny, secure, URL-friendly, unique string ID generator for JavaScript.
- [OpenAI API](https://platform.openai.com/api-keys) - provides an API key that enables you to carry out various tasks using ChatGPT models.
- [CopilotKit](https://github.com/CopilotKit) - an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.

## Project Set up and Package Installation

First, create a Next.js application by running the code snippet below in your terminal:

```tsx
npx create-next-app@latest todolistgenerator
```

Select your preferred configuration settings. For this tutorial, we'll be using TypeScript and Next.js App Router.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0tc6he9eivkt3hhxnj3p.png)

Next, install Nanoid package and its dependancies.

```tsx
npm i nanoid
```

Finally, install the CopilotKit packages. These packages enable us to retrieve data from the React state and add AI copilot to the application.

```tsx
 npm install @copilotkit/react-ui @copilotkit/react-textarea @copilotkit/react-core @copilotkit/backend @copilotkit/shared
```

Congratulations! You're now ready to build an AI-powered to-do list generator.

## **Building The To-Do List Generator Frontend**

In this section, I will walk you through the process of creating the to-do list generator frontend with static content to define the generator’s user interface.

To get started, go to `/[root]/src/app` in your code editor and create a folder called `types`. Inside the types folder, create a file named `todo.ts` and add the following code that defines a TypeScript interface called **`Todo`.** 

The **`Todo`** interface defines an object structure where every todo item must have an **`id`**, **`text`**, and **`isCompleted`** status, while it may optionally have an **`assignedTo`** property.

```tsx
export interface Todo {
  id: string;
  text: string;
  isCompleted: boolean;
  assignedTo?: string;
}

```

Then go to `/[root]/src/app` in your code editor and create a folder called `components`. Inside the components folder, create three files named `Header.tsx`, `TodoList.tsx` and `TodoItem.tsx` .

In the `Header.tsx` file, add the following code that defines a functional component named `Header` that will render the generator’s navbar.

```tsx
import Link from "next/link";

export default function Header() {
  return (
    <>
      <header className="flex flex-wrap sm:justify-start sm:flex-nowrap z-50 w-full bg-gray-800 border-b border-gray-200 text-sm py-3 sm:py-0 ">
        <nav
          className="relative max-w-7xl w-full mx-auto px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8"
          aria-label="Global">
          <div className="flex items-center justify-between">
            <Link
              className="w-full flex-none text-xl text-white font-semibold p-6"
              href="/"
              aria-label="Brand">
              To-Do List Generator
            </Link>
          </div>
        </nav>
      </header>
    </>
  );
}

```

In the `TodoItem.tsx` file, add the following code that defines a React functional component called **`TodoItem`**. It uses TypeScript to ensure type safety and to define the props that the component accepts.

```tsx
import { Todo } from "../types/todo"; // Importing the Todo type from a types file

// Defining the interface for the props that the TodoItem component will receive
interface TodoItemProps {
  todo: Todo; // A single todo item
  toggleComplete: (id: string) => void; // Function to toggle the completion status of a todo
  deleteTodo: (id: string) => void; // Function to delete a todo
  assignPerson: (id: string, person: string | null) => void; // Function to assign a person to a todo
  hasBorder?: boolean; // Optional prop to determine if the item should have a border
}

// Defining the TodoItem component as a functional component with the specified props
export const TodoItem: React.FC<TodoItemProps> = ({
  todo,
  toggleComplete,
  deleteTodo,
  assignPerson,
  hasBorder,
}) => {
  return (
    <div
      className={
        "flex items-center justify-between px-4 py-2 group" +
        (hasBorder ? " border-b" : "") // Conditionally adding a border class if hasBorder is true
      }>
      <div className="flex items-center">
        <input
          className="h-5 w-5 text-blue-500"
          type="checkbox"
          checked={todo.isCompleted} // Checkbox is checked if the todo is completed
          onChange={() => toggleComplete(todo.id)} // Toggle completion status on change
        />
        <span
          className={`ml-2 text-sm text-white ${
            todo.isCompleted ? "text-gray-500 line-through" : "text-gray-900" // Apply different styles if the todo is completed
          }`}>
          {todo.assignedTo && (
            <span className="border rounded-md text-xs py-[2px] px-1 mr-2  border-purple-700 uppercase bg-purple-400 text-black font-medium">
              {todo.assignedTo} {/* Display the assigned person's name if available */}
            </span>
          )}
          {todo.text} {/* Display the todo text */}
        </span>
      </div>
      <div>
        <button
          onClick={() => deleteTodo(todo.id)} // Delete the todo on button click
          className="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            strokeWidth={1.5}
            stroke="currentColor"
            className="w-5 h-5">
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
            />
          </svg>
        </button>
        <button
          onClick={() => {
            const name = prompt("Assign person to this task:");
            assignPerson(todo.id, name);
          }}
          className="ml-2 text-blue-500 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            strokeWidth={1.5}
            stroke="currentColor"
            className="w-5 h-5">
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
            />
          </svg>
        </button>
      </div>
    </div>
  );
};

```

In the `TodoList.tsx` file, add the following code that defines a React functional component named **`TodoList`**. This component is used to manage and display a list of to-do items.

```tsx
"use client";

import { TodoItem } from "./TodoItem"; // Importing the TodoItem component
import { nanoid } from "nanoid"; // Importing the nanoid library for generating unique IDs
import { useState } from "react"; // Importing the useState hook from React
import { Todo } from "../types/todo"; // Importing the Todo type

// Defining the TodoList component as a functional component
export const TodoList: React.FC = () => {
   // State to hold the list of todos
  const [todos, setTodos] = useState<Todo[]>([]);
  // State to hold the current input value
  const [input, setInput] = useState("");
  
  
   // Function to add a new todo
  const addTodo = () => {
    if (input.trim() !== "") {
      // Check if the input is not empty
      const newTodo: Todo = {
        id: nanoid(), // Generate a unique ID for the new todo
        text: input.trim(), // Trim the input text
        isCompleted: false, // Set the initial completion status to false
      };
      setTodos([...todos, newTodo]); // Add the new todo to the list
      setInput(""); // Clear the input field
    }
  };

  // Function to handle key press events
  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") {
      // Check if the Enter key was pressed
      addTodo(); // Add the todo
    }
  };
  
  // Function to toggle the completion status of a todo
  const toggleComplete = (id: string) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
      )
    );
  };
	
	// Function to delete a todo
  const deleteTodo = (id: string) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

	// Function to assign a person to a todo
  const assignPerson = (id: string, person: string | null) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id
          ? { ...todo, assignedTo: person ? person : undefined }
          : todo
      )
    );
  };

  return (
    <div>
      <div className="flex mb-4">
        <input
          className="border rounded-md p-2 flex-1 mr-2"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={handleKeyPress} // Add this to handle the Enter key press
        />
        <button
          className="bg-blue-500 rounded-md p-2 text-white"
          onClick={addTodo}>
          Add Todo
        </button>
      </div>
      {todos.length > 0 && ( // Check if there are any todos
        <div className="border rounded-lg">
          {todos.map((todo, index) => (
            <TodoItem
              key={todo.id} // Unique key for each todo item
              todo={todo} // Pass the todo object as a prop
              toggleComplete={toggleComplete} // Pass the toggleComplete function as a prop
              deleteTodo={deleteTodo} // Pass the deleteTodo function as a prop
              assignPerson={assignPerson} // Pass the assignPerson function as a prop
              hasBorder={index !== todos.length - 1} // Conditionally add a border to all but the last item
            />
          ))}
        </div>
      )}
     </div>
  );
};

```

Next, go to `/[root]/src/page.tsx` file, and add the following code that imports `TodoList` and `Header` components and defines a functional component named `Home`.

```tsx
import Header from "./components/Header";
import { TodoList } from "./components/TodoList";

export default function Home() {
  return (
    <>
      <Header />
      <div className="border rounded-md max-w-2xl mx-auto p-4 mt-4">
        <h1 className="text-2xl text-white font-bold mb-4">
          Create a to-do list
        </h1>
        <TodoList />
      </div>
    </>
  );
}

```

Next, remove the CSS code in the `globals.css` file and add the following CSS code.

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  height: 100vh;
  background-color: rgb(16, 23, 42);
}
```

Finally, run the command `npm run dev` on the command line and then navigate to http://localhost:3000/.

Now you should view the To-Do List generator frontend on your browser, as shown below.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wmd8j3brrtiqackalul1.png)

## **Integrating AI Functionalities To The Todo List Generator Using CopilotKit**

In this section, you will learn how to add an AI copilot to the To-Do List generator to generate lists using CopilotKit.

CopilotKit offers both frontend and [backend](https://docs.copilotkit.ai/getting-started/quickstart-backend) packages. They enable you to plug into the React states and process application data on the backend using AI agents.

First, let's add the CopilotKit React components to the To-Do List generator frontend.

### **Adding CopilotKit to the To-Do List Generator Frontend**

Here, I will walk you through the process of integrating the To-Do List generator with the CopilotKit frontend to facilitate lists generation.

To get started, use the code snippet below to import `useCopilotReadable`, and `useCopilotAction`, custom hooks at the top of the `/[root]/src/app/components/TodoList.tsx` file.

```tsx
import { useCopilotAction, useCopilotReadable } from "@copilotkit/react-core";
```

Inside the `TodoList` function, below the state variables, add the following code that uses the `useCopilotReadable` hook to add the to-do lists that will be generated as context for the in-app chatbot. The hook makes the to-do lists readable to the copilot.

```tsx
useCopilotReadable({
    description: "The user's todo list.",
    value: todos,
  });
```

Below the code above, add the following code that uses the `useCopilotAction` hook to set up an action called `updateTodoList` which will enable the generation of to-do lists.

The action takes one parameter called items which enables the generation of todo lists and contains a handler function that generates todo lists based on a given prompt.

Inside the handler function, `todos` state is updated with the newly generated todo list, as shown below.

```tsx
// Define the "updateTodoList" action using the useCopilotAction function
  useCopilotAction({
    // Name of the action
    name: "updateTodoList",

    // Description of what the action does
    description: "Update the users todo list",

    // Define the parameters that the action accepts
    parameters: [
      {
        // The name of the parameter
        name: "items",

        // The type of the parameter, an array of objects
        type: "object[]",

        // Description of the parameter
        description: "The new and updated todo list items.",

        // Define the attributes of each object in the items array
        attributes: [
          {
            // The id of the todo item
            name: "id",
            type: "string",
            description:
              "The id of the todo item. When creating a new todo item, just make up a new id.",
          },
          {
            // The text of the todo item
            name: "text",
            type: "string",
            description: "The text of the todo item.",
          },
          {
            // The completion status of the todo item
            name: "isCompleted",
            type: "boolean",
            description: "The completion status of the todo item.",
          },
          {
            // The person assigned to the todo item
            name: "assignedTo",
            type: "string",
            description:
              "The person assigned to the todo item. If you don't know, assign it to 'YOU'.",

            // This attribute is required
            required: true,
          },
        ],
      },
    ],

    // Define the handler function that executes when the action is invoked
    handler: ({ items }) => {
      // Log the items to the console for debugging purposes
      console.log(items);

      // Create a copy of the existing todos array
      const newTodos = [...todos];

      // Iterate over each item in the items array
      for (const item of items) {
        // Find the index of the existing todo item with the same id
        const existingItemIndex = newTodos.findIndex(
          (todo) => todo.id === item.id
        );

        // If an existing item is found, update it
        if (existingItemIndex !== -1) {
          newTodos[existingItemIndex] = item;
        }
        // If no existing item is found, add the new item to the newTodos array
        else {
          newTodos.push(item);
        }
      }

      // Update the state with the new todos array
      setTodos(newTodos);
    },

    // Provide feedback or a message while the action is processing
    render: "Updating the todo list...",
  });
```

Below the code above, add the following code that uses the `useCopilotAction` hook to set up an action called `deleteTodo` which enables you to delete a to-do item.

The action takes a parameter called id which enables you to delete a todo item by id and contains a handler function that updates the todos state by filtering out the deleted todo item with the given id.

```tsx
// Define the "deleteTodo" action using the useCopilotAction function
  useCopilotAction({
    // Name of the action
    name: "deleteTodo",

    // Description of what the action does
    description: "Delete a todo item",

    // Define the parameters that the action accepts
    parameters: [
      {
        // The name of the parameter
        name: "id",

        // The type of the parameter, a string
        type: "string",

        // Description of the parameter
        description: "The id of the todo item to delete.",
      },
    ],

    // Define the handler function that executes when the action is invoked
    handler: ({ id }) => {
      // Update the state by filtering out the todo item with the given id
      setTodos(todos.filter((todo) => todo.id !== id));
    },

    // Provide feedback or a message while the action is processing
    render: "Deleting a todo item...",
  });
```

After that, go to `/[root]/src/app/page.tsx` file and import CopilotKit frontend packages and styles at the top using the code below.

```tsx
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
```

Then use `CopilotKit` to wrap the `CopilotPopup` and `TodoList` components, as shown below. The `CopilotKit` component specifies the URL for CopilotKit's backend endpoint (`/api/copilotkit/`) while the `CopilotPopup` renders the in-app chatbot that you can give prompts to generate todo lists.

```tsx
export default function Home() {
  return (
    <>
      <Header />
      <div className="border rounded-md max-w-2xl mx-auto p-4 mt-4">
        <h1 className="text-2xl text-white font-bold mb-4">
          Create a to-do list
        </h1>
        <CopilotKit runtimeUrl="/api/copilotkit">
          <TodoList />

          <CopilotPopup
            instructions={
              "Help the user manage a todo list. If the user provides a high level goal, " +
              "break it down into a few specific tasks and add them to the list"
            }
            defaultOpen={true}
            labels={{
              title: "Todo List Copilot",
              initial: "Hi you! 👋 I can help you manage your todo list.",
            }}
            clickOutsideToClose={false}
          />
        </CopilotKit>
      </div>
    </>
  );
}
```

After that, run the development server and navigate to [http://localhost:3000](http://localhost:3000/). You should see that the in-app chatbot was integrated into the todo list generator.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ryeqnymp5lm397thpn5f.png)

### **Adding CopilotKit Backend to the Blog**

Here, I will walk you through the process of integrating the todo lists generator with the CopilotKit backend that handles requests from frontend, and provides function calling and various LLM backends such as GPT.

To get started, create a file called `.env.local` in the root directory. Then add the environment variable below in the file that holds your `ChatGPT`  API keys.

```jsx
OPENAI_API_KEY="Your ChatGPT API key”
```

To get the ChatGPT API key, navigate to https://platform.openai.com/api-keys.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0bibiuouk5wvrxcuy2g.jpeg)

After that, go to `/[root]/src/app` and create a folder called `api`. In the `api` folder, create a folder called `copilotkit`.

In the `copilotkit` folder, create a file called `route.ts` that contains code that sets up a backend functionality to process POST requests.

```tsx
// Import the necessary modules from the "@copilotkit/backend" package
import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/backend";

// Define an asynchronous function to handle POST requests
export async function POST(req: Request): Promise<Response> {
  // Create a new instance of CopilotRuntime
  const copilotKit = new CopilotRuntime({});

  // Use the copilotKit to generate a response using the OpenAIAdapter
  // Pass the incoming request (req) and a new instance of OpenAIAdapter to the response method
  return copilotKit.response(req, new OpenAIAdapter());
}

```

## How To Generate Todo Lists

Now go to the in-app chatbot you integrated earlier and give it a prompt like, “I want to go to the gym to do a full body workout. add to the list workout routine I should follow”

Once it is done generating, you should see the list of full-body workout routine you should follow, as shown below.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/48n9ssymxhm5i2yv0pdf.png)

You can assign the to-do list to someone by giving the chatbot a prompt like, “assign the to-do list to Doe.”


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6r1f816c1o8da5z0t1kk.png)

You can mark the to-do list as completed by giving the chatbot a prompt like, “mark the to-do list as completed.”


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qx2wpg6ee7hswjl68t4q.png)

You can delete the to-do list by giving the chatbot a prompt like, “delete the todo list.”


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ksoj6mtlxt8ag8opsocs.png)

Congratulations! You’ve completed the project for this tutorial.

## Conclusion

[CopilotKit](https://copilotkit.ai/) is an incredible tool that allows you to add AI Copilots to your products within minutes. Whether you're interested in AI chatbots and assistants or automating complex tasks, CopilotKit makes it easy.

If you need to build an AI product or integrate an AI tool into your software applications, you should consider CopilotKit.

You can find the source code for this tutorial on GitHub: https://github.com/TheGreatBonnie/AIpoweredToDoListGenerator