How to build: a To-Do list app with an embedded AI copilot (Next.js, GPT4, & CopilotKit)
Bonnie
Posted on May 29, 2024
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 🪄.
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.
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 🤖
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
Select your preferred configuration settings. For this tutorial, we'll be using TypeScript and Next.js App Router.
Next, install Nanoid package and its dependancies.
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.
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.
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.
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.
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.
"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
.
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.
@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.
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";
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,
});
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...",
});
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...",
});
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";
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>
</>
);
}
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.
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”
To get the ChatGPT API key, navigate to https://platform.openai.com/api-keys.
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());
}
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.
You can assign the to-do list to someone by giving the chatbot a prompt like, “assign the to-do list to Doe.”
You can mark the to-do list as completed by giving the chatbot a prompt like, “mark the to-do list as completed.”
You can delete the to-do list by giving the chatbot a prompt like, “delete the todo list.”
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
Posted on May 29, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.