TL;DR
In this tutorial, you'll learn what Generative UI is and how to use it to provide a dynamic user experience within your apps.
You'll also learn how to build an interactive sales dashboard that allows you to add, update, and delete data using an AI copilot.
This practical guide will bring up to date to the cutting edge of frontend & AI-enabled application development. Your users will be grateful you honed these skills.
CopilotKit: The framework for building in-app AI copilots
CopilotKit is anΒ 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 π€
What is Generative UI?
Generative UI refers to UI components that are dynamically generated and updated in real-time based on users' inputs. The AI-embedded software application listens to a user's prompt and generates a UI based on the given instruction. CopilotKit uses this feature to enable users to render React components in the copilot chat window.
Prerequisites
To fully understand this tutorial, you need to have a basic understanding of React or Next.js.
We'll also make use of the following tools:
CopilotKit - an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.
OpenAI API Key - provides an API key that enables us to carry out various tasks using ChatGPT models.
Shadcn/ui - a collection of customizable and reusable UI components.
Recharts - a chart library built specifically for React applications
Project Set up and Package Installation
First, create a Next.js application by running the following code snippet in your terminal:
npx create-next-app generative-ui-with-copilotkit
Install the CopilotKit packages. These packages enable the AI copilot to retrieve data from the React state and make decisions within the application.
npm install @copilotkit/react-ui @copilotkit/react-core @copilotkit/backend recharts
Set up the shadcn/ui
within the Next.js project by running the code snippet below:
npx shadcn-ui@latest init
Configure the components.json
file by answering the following installation 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? βΊ > yes
Congratulations! You should now have a components.json
file within your Next.js project that outlines the configurations for Shadcn UI and components
folder that will contain the various UI components.
Finally, add the following ShadCn components used within the application: Card, Chart, Checkbox, and Table components to the application.
npx shadcn-ui@latest add card
npx shadcn-ui@latest add chart
npx shadcn-ui@latest add checkbox
npx shadcn-ui@latest add table
Building the Sales Dashboard with Next.js
In this section, you'll learn how to create a sales dashboard that visualizes data using the ShadCn UI components.
First, create a types.d.ts
file at the root of the Next.js project and copy the following code snippet into the file:
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface Invoice {
id: number;
status: "Paid" | "Pending" | "Overdue";
amount: number;
method: "Credit Card" | "Paypal" | "Bank Transfer";
}
interface Chart {
month: string;
sales: number;
customers: number;
}
The code snippet above defines the data structure of the various variables used within the application.
Add a components
folder within the Next.js app folder and create App, Card, Chart, Checkbox, Nav, and Table components.
cd app
mkdir components && cd components
touch App.tsx Card.tsx Chart.tsx Checkbox.tsx Nav.tsx Table.tsx
Update the App.tsx
component to contain the necessary React states and function:
import { useState } from "react";
import ChartComponent from "@/app/components/Chart";
import CardComponent from "@/app/components/Card";
import TableComponent from "@/app/components/Table";
import CheckboxComponent from "@/app/components/Checkbox";
import NavComponent from "@/app/components/Nav";
export default function App() {
//ππ» a todo list
const [todoList, setTodoList] = useState<Todo[]>([
{
id: 1,
text: "Learn about CopilotKit implementation",
completed: false,
},
{
id: 2,
text: "Remind Uli about the next project",
completed: false,
},
{
id: 3,
text: "Send an invoice to CopilotKit team",
completed: false,
},
]);
//ππ» an invoice list
const [invoiceList, setInvoiceList] = useState<Invoice[]>([
{
id: 1,
status: "Pending",
amount: 1000,
method: "Credit Card",
},
{
id: 2,
status: "Paid",
amount: 2000,
method: "Paypal",
},
{
id: 3,
status: "Overdue",
amount: 3000,
method: "Bank Transfer",
},
]);
//ππ» the chart data
const [chartData, setChartData] = useState<Chart[]>([
{ month: "January", sales: 350, customers: 80 },
{ month: "February", sales: 200, customers: 30 },
{ month: "March", sales: 1500, customers: 120 },
{ month: "April", sales: 1050, customers: 190 },
{ month: "May", sales: 1200, customers: 130 },
{ month: "June", sales: 550, customers: 140 },
{ month: "July", sales: 1200, customers: 130 },
]);
//ππ» calculates the total sales and number of customers
const calculateTotal = (key: keyof Chart): number => {
if (key === "sales")
return chartData.reduce((acc, item) => acc + item.sales, 0);
return chartData.reduce((acc, item) => acc + item.customers, 0);
};
return (/**-- ππ» UI components ππΌ ---*/)
}
Render the following UI components from the App component, each designed to display the invoices, to-do tasks, and sales data.
export default function App() {
//ππ» the states and functions
return (
<main>
<NavComponent />
<div className='w-full flex items-center justify-between p-4 md:flex-row space-x-4'>
<div className='lg:w-1/2 h-[300px] lg:mb-0 mb-4 w-full'>
<CardComponent
invoiceLength={invoiceList.length}
todoLength={todoList.length}
totalCustomers={calculateTotal("customers")}
totalSales={calculateTotal("sales")}
/>
</div>
<div className='lg:w-1/2 h-[300px] w-full lg:mb-0 mb-4 '>
<ChartComponent chartData={chartData} />
</div>
</div>
<div className='w-full flex flex-row items-center justify-between lg:space-x-4 p-4'>
<div className='lg:w-1/2 w-full h-full lg:mb-0 mb-8'>
<TableComponent invoiceList={invoiceList} />
</div>
<div className='lg:w-1/2 w-full h-full lg:mb-0 mb-4'>
<CheckboxComponent todoList={todoList} setTodoList={setTodoList} />
</div>
</div>
</main>
);
}
Finally, you can copy the various dashboard components from the GitHub repository.
How to Add CopilotKit to a Next.js application
In this section, you'll learn how to add CopilotKit to the application to enable users to add, delete, and update the data automatically using AI copilots.
Before we proceed, visit the OpenAI Developers' Platform and create a new secret key.
Create a .env.local
file and copy the your newly created secret key into the file.
OPENAI_API_KEY=<YOUR_OPENAI_SECRET_KEY>
OPENAI_MODEL=gpt-4-1106-preview
Next, you need to create an API endpoint for CopilotKit. Within the Next.js app folder, create an api/copilotkit
folder containing a route.ts
file.
cd app
mkdir api && cd api
mkdir copilotkit && cd copilotkit
touch route.ts
Copy the code snippet below into the route.ts
file. The CopilotKit backend accept usersβ requests and make decisions using the OpenAI model.
import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/backend";
export const runtime = "edge";
export async function POST(req: Request): Promise<Response> {
const copilotKit = new CopilotRuntime({});
const openaiModel = process.env["OPENAI_MODEL"];
return copilotKit.response(req, new OpenAIAdapter({ model: openaiModel }));
}
To connect the application to the backend API route, copy the code snippet below into the app/page.tsx
file.
"use client"
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import "@copilotkit/react-textarea/styles.css";
import App from "./components/App";
export default function Home() {
return (
<CopilotKit runtimeUrl='/api/copilotkit/'>
<App />
<CopilotPopup
instructions='Help the user update and manipulate data on the chart, table, todo, and card components.'
defaultOpen={true}
labels={{
title: "Data Visualization Copilot",
initial:
"Hello there! I can help you add, edit, and remove data from the various components on the page. You can update the chat, table, and todo list. Let's get started!",
}}
clickOutsideToClose={false}
></CopilotPopup>
</CopilotKit>
);
}
The CopilotKit component wraps the entire application and accepts a runtimeUrl
prop that contains a link to the API endpoint. The CopilotKitPopup
component adds a chatbot sidebar panel to the application, enabling us to provide various instructions and perform various actions using the AI copilot.
Leveraging Generative UI and CopilotKit for Interactive Actions
CopilotKit provides two hooks that enable us to handle user's request and plug into the application state: useCopilotAction
and useCopilotReadable
.
The useCopilotAction
hook allows you to define actions to be carried out by CopilotKit. It accepts an object containing the following parameters:
- name - the action's name.
- description - the action's description.
- parameters - an array containing the list of the required parameters.
- render - a string or function that returns a UI component.
- handler - the executable function that is triggered by the action.
useCopilotAction({
name: "sayHello",
description: "Say hello to someone.",
parameters: [
{
name: "name",
type: "string",
description: "name of the person to say greet",
},
],
render: "Process greeting message...",
handler: async ({ name }) => {
alert(`Hello, ${name}!`);
},
});
The render
attribute allows us to dynamically display React components based on user input and the current status of an action, facilitating the use of Generative UI with CopilotKit. You'll learn how to implement this feature shortly.
The useCopilotReadable
hook provides the application state to CopilotKit.
import { useCopilotReadable } from "@copilotkit/react-core";
const myAppState = "...";
useCopilotReadable({
description: "The current state of the app",
value: myAppState
});
Now, letβs plug the application states into CopilotKit.
Within the App.tsx
component, pass the chartData
, invoiceList
, and todoList
states into CopilotKit.
//ππ» pass Chart data to CopilotKit
useCopilotReadable({
description:
"The chart data is a list of sales and customers data for each month. You can update the data for each month. It contains the month, sales, and customers data.",
value: chartData,
});
//ππ» pass invoice data to CopilotKit
useCopilotReadable({
description: "The invoice list is a list of invoices that need to be paid. You can add, edit, and remove invoices from the list and also update the status of the invoice. An invoice status can either be Paid, Pending, or Overdue. The acceptable payment methods are Credit Card, Paypal, and Bank Transfer.",
value: invoiceList,
});
//ππ» pass todolist data to CopilotKit
useCopilotReadable({
description: "The todo list is a list of tasks that need to be completed. You can add, edit, and remove tasks from the list.",
value: todoList,
});
Automating Various Actions with CopilotKit
We need to allow users to create, update, and delete data from the application. Therefore, let's create actions that do the following using the useCopilotAction
hook:
- update the chart data,
- create new invoices,
- delete an invoice,
- update a todo status,
- create new todo,
- delete a todo.
Add an action that updates the chartData
to the App.tsx
file:
//ππ» action to update chartData
useCopilotAction({
name: "updateChartData",
description: "Update the chart data for the a particular month.",
parameters: [
{
name: "month",
type: "string",
description: "The month to update the data for.",
required: true,
},
{
name: "sales",
type: "number",
description: "The sales data for the month.",
required: true,
},
{
name: "customers",
type: "number",
description: "The customers data for the month.",
required: true,
},
],
render: ({ status, args }) => {
const { month, sales, customers } = args;
if (month === undefined || sales === undefined || customers === undefined) return "";
if (typeof month !== "string" || typeof sales !== "number" || typeof customers !== "number") return "";
const updateChart = () => {
setChartData((prev) => {
return prev.map((item) => {
if (item.month === month) {
return { month, sales, customers };
}
return item;
});
});
};
return (
<div className="w-full p-2">
<p className="text-sm text-blue-400 mb-2">Status: {status}</p>
<ChartComponent chartData={[{month, sales, customers}]} />
<button className="px-4 py-2 bg-blue-400 text-white shadow rounded-md" onClick={updateChart}>Update</button>
</div>
)
},
handler: async () => {
// Do nothing
},
});
- From the code snippet above,
- The action accepts an array of parameters that describes the attributes of the
chartData
React state. - The
render
property displays the result of the user's prompt and a button that allows the user to add the data to the page.
- The action accepts an array of parameters that describes the attributes of the
Next, create the actions that create and delete invoices from the application.
//ππ» action to add new invoice
useCopilotAction({
name: "addNewInvoice",
description: "Add new invoices to the invoice list",
parameters: [
{
name: "status",
type: "string",
description: "The status of the invoice.",
required: true,
},
{
name: "amount",
type: "number",
description: "The amount of the invoice.",
required: true,
},
{
name: "method",
type: "string",
description: "The payment method of the invoice.",
required: true,
},
],
render: ({ status: fetchStatus, args }) => {
const { amount, method, status } = args;
if (method !== "Credit Card" && method !== "Paypal" && method !== "Bank Transfer") return "";
if (status !== "Paid" && status !== "Pending" && status !== "Overdue") return "";
if (amount === undefined) return "";
const addInvoice = () => {
setInvoiceList((prev ) => {
return [...prev, { id: prev.length + 1, status, amount, method }];
});
};
return (
<div className="w-full p-2">
<p className="text-sm text-blue-400 mb-2">Status: {fetchStatus}</p>
{status && amount !== undefined && method && <TableComponent invoiceList={[{ id: invoiceList.length + 1, status, amount, method }]} />}
<button className="px-4 py-2 bg-blue-400 text-white shadow rounded-md" onClick={addInvoice}>Add to Page</button>
</div>
)
},
handler: async ({ status, amount, method }) => {
//Do nothing
})
//ππ» action to delete invoices
useCopilotAction({
name: "deleteInvoice",
description: "Remove invoices to the invoice list",
parameters: [
{
name: "id",
type: "number",
description: "The id of the invoice to remove.",
required: true,
},
],
render: ({ status, args }) => {
const { id } = args;
if (id === undefined) return "";
const getInvoice = invoiceList.find((item) => item.id === id);
if (!getInvoice) return ""
const deleteInvoice = () => {
setInvoiceList((prev) => {
return prev.filter((item) => item.id !== id);
});
}
return (
<div className="w-full p-2">
<p className="text-sm text-blue-400 mb-2">Status: {status}</p>
<TableComponent invoiceList={[getInvoice]} />
<button className="px-4 py-2 bg-blue-400 text-white shadow rounded-md" onClick={deleteInvoice}>Delete</button>
</div>
)
},
handler: async ({) => {
// Do nothing
}
})
The addNewInvoice
and deleteInvoice
actions render React components (Generative UI) after executing the user's requests, allowing the user to add the result to the page by clicking a button.
Finally, add the actions that create, update, and delete todos.
//ππ» action to update todo status
useCopilotAction({
name: "toggleTodo",
description: "Toggle the completion status of a todo item.",
parameters: [
{
name: "id",
type: "number",
description: "The id of the todo item to toggle.",
required: true,
},
],
render: ({ status, args }) => {
const { id } = args;
if (id === undefined) return "";
const getTodo = todoList.find((item) => item.id === id);
if (!getTodo) return "";
const toggleTodo = () => {
setTodoList(
todoList.map((todo) => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo;
})
);
};
return (
<div className="w-full p-2">
<p className="text-sm text-blue-400 mb-2">Status: {status}</p>
<CheckboxComponent todoList={[getTodo]} setTodoList={setTodoList} />
<button className="px-4 py-2 bg-blue-400 text-white shadow rounded-md" onClick={toggleTodo}>Toggle Todo</button>
</div>
)
},
handler: async () => {
// Do nothing
},
})
//ππ» action to add new todo
useCopilotAction({
name: "addNewTodo",
description: "Add new todo to the todo list",
parameters: [
{
name: "text",
type: "string",
description: "The text of the todo item.",
required: true,
},
],
render: ({ status, args }) => {
const { text } = args;
if (text === undefined) return "";
const addTodo = () => {
setTodoList((prev) => {
return [...prev, { id: prev.length + 1, text, completed: false }];
});
};
return (
<div className="w-full p-2">
<p className="text-sm text-blue-400 mb-2">Status: {status}</p>
<CheckboxComponent todoList={[...todoList, { id: todoList.length + 1, text, completed: false }]} setTodoList={setTodoList} />
<button className="px-4 py-2 bg-blue-400 text-white shadow rounded-md" onClick={addTodo}>Add to Page</button>
</div>
)
},
handler: async () => {
// Do nothing
},
});
//ππ» action to delete todo
useCopilotAction({
name: "deleteTodo",
description: "Remove todo from the todo list",
parameters: [
{
name: "id",
type: "number",
description: "The id of the todo item to remove.",
required: true,
},
],
render: ({ status, args }) => {
const { id } = args;
if (id === undefined) return "";
const getTodo = todoList.find((item) => item.id === id);
if (!getTodo) return "";
const deleteTodo = () => {
setTodoList((prev) => {
return prev.filter((item) => item.id !== id);
});
};
return (
<div className="w-full p-2">
<p className="text-sm text-blue-400 mb-2">Status: {status}</p>
<CheckboxComponent todoList={[getTodo]} setTodoList={setTodoList} />
<button className="px-4 py-2 bg-red-500 text-white shadow rounded-md" onClick={deleteTodo}>Delete</button>
</div>
)
},
handler: async ({ id }) => {
// Do nothing
},
});
The toggleTodo
action toggles the status of a todo. The addNewTodo
action creates a new todo, and the deleteTodo
action deletes a todo via its ID.
Congratulations! Youβve completed the project for this tutorial.
Here is a brief demo of the application:
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/dha-stix/interactive-sales-dashboard-with-copilokit
Thank you for reading!
Top comments (30)
I was somewhat disappointed, since I actually expected a game (from reading the title) :D
Not a bad idea for a future article!
Same
Hope you enjoy!
Check out the source code here:
github.com/dha-stix/interactive-sa...
CopilotKit Generative UI docs page:
docs.copilotkit.ai/concepts/genera...
Love the generative UI feature!
Makes the in-app AI functionalities pop!
What if I am not from the US, can I still use it?
It's completely open-source. You could use it on Mars π πͺ
Wow, this is really something out of the ordinary. I love it. It would be safe to say that I am now tempted to use CopilotKit on one of my coming side projects. π€
Love to hear it!
Join our discord if you need support when building:
discord.gg/6dffbvGU3D
Thanks, @uliyahoo, for the server link. Iβll definitely join the server once I start building with CopilotKit. This is a really great project you guys are working on. It seems to be very actively developed. I hope it reaches even more devs! π
Great read. I always appreciate articles that include a demo, to get the idea yk.
There are so many possibilities with Copilotkit, including creating some awesome SaaS apps. I will create one for sure in the future :)
Thanks Anmol! Means a lot coming from you. Agreed about articles including a demo.
I've been experimenting with CopilotKit recently, and it's impressive how much time it saves and its exceptional out-of-the-box UI!
Thanks Nathan :)
Another great article!
The cover image is also really cool! π
Thanks Ekeimini! Haha glad you liked the cover photo, wasn't sure about this one.
Freaking cool!
π
This is very insightful, thanks for sharing with the community
Thanks for the kind words!
Interesting...
π§
Some comments may only be visible to logged-in visitors. Sign in to view all comments.