How to build: a To-Do list app with an embedded AI copilot (Next.js, GPT4, & CopilotKit)

Bonnie - May 29 - - Dev Community

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

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


CopilotKit: The framework for building in-app AI copilots

CopilotKit is an open-source AI copilot framework. 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 ⭐️


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 - a tiny, secure, URL-friendly, unique string ID generator for JavaScript.
  • OpenAI API - provides an API key that enables you to carry out various tasks using ChatGPT models.
  • 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:



npx create-next-app@latest todolistgenerator


Enter fullscreen mode Exit fullscreen mode

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

Image description

Next, install Nanoid package and its dependancies.



npm i nanoid


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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.



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



Enter fullscreen mode Exit fullscreen mode

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.



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>
    </>
  );
}



Enter fullscreen mode Exit fullscreen mode

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.



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>
  );
};



Enter fullscreen mode Exit fullscreen mode

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.



"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>
  );
};



Enter fullscreen mode Exit fullscreen mode

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.



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>
    </>
  );
}



Enter fullscreen mode Exit fullscreen mode

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



@tailwind base;
@tailwind components;
@tailwind utilities;

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


Enter fullscreen mode Exit fullscreen mode

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

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 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.



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


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

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.



// 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...",
  });


Enter fullscreen mode Exit fullscreen mode

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.



// 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...",
  });


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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.



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>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode

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

Image description

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.



OPENAI_API_KEY="Your ChatGPT API key”


Enter fullscreen mode Exit fullscreen mode

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

Image description

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.



// 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());
}

Enter fullscreen mode Exit fullscreen mode




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

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

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

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

Image description

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

Conclusion

CopilotKit 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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .