端到端展示如何用Next.js、OpenAI和CopilotKit实现AI辅导体验,298次高热度反馈验证实用性;对全栈开发者直接可用。
在本文中,你将学会如何构建一个 AI 驱动的营销活动管理应用,允许你创建和分析广告活动,帮助你为业务做出正确的决策。
CopilotKit 是一个开源 AI 副驾驶平台。我们让将强大的 AI 整合到你的 React 应用中变得简单易行。
要充分理解本教程,你需要具备 React 或 Next.js 的基本知识。
我们还将使用以下工具:
首先,通过在终端运行以下代码片段来创建 Next.js 应用:
npx create-next-app campaign-manager
选择你偏好的配置设置。在本教程中,我们将使用 TypeScript 和 Next.js App Router。
接下来,将 Heroicons、Radix UI 及其原始组件安装到项目中。
npm install @heroicons/react @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-icons @radix-ui/react-label @radix-ui/react-popover @radix-ui/react-select @radix-ui/react-slot @radix-ui/react-tabs
同时,安装 Recharts 库(一个用于创建交互式图表的 React 库)和以下实用工具包:
npm install recharts class-variance-authority clsx cmdk date-fns lodash react-day-picker tailwind-merge tailwindcss-animate
最后,安装 CopilotKit 包。这些包使 AI 副驾驶能够从 React 状态检索数据并在应用内做出决策。
npm install @copilotkit/react-ui @copilotkit/react-textarea @copilotkit/react-core @copilotkit/backend
恭喜!你现在已准备好构建应用。
在本部分中,我将指导你构建营销活动管理应用的用户界面。
首先,让我们做一些初始设置。
在 src 文件夹内创建 components 和 lib 文件夹。
cd src
mkdir components lib
在 lib 文件夹内,我们将声明静态类型和应用的默认活动。因此,在 lib 文件夹内创建 data.ts 和 types.ts 文件。
cd lib
touch data.ts type.ts
将以下代码片段复制到 type.ts 文件中。它声明了活动属性及其数据类型。
export interface Campaign {
id: string;
objective?:
| "brand-awareness"
| "lead-generation"
| "sales-conversion"
| "website-traffic"
| "engagement";
title: string;
keywords: string;
url: string;
headline: string;
description: string;
budget: number;
bidStrategy?: "manual-cpc" | "cpa" | "cpm";
bidAmount?: number;
segment?: string;
}
为应用创建默认活动列表,并将其复制到 data.ts 文件中。
import { Campaign } from "./types";
export let DEFAULT_CAMPAIGNS: Campaign[] = [
{
id: "1",
title: "CopilotKit",
url: "https://www.copilotkit.ai",
headline: "Copilot Kit - The Open-Source Copilot Framework",
description:
"Build, deploy, and operate fully custom AI Copilots. In-app AI chatbots, AI agents, AI Textareas and more.",
budget: 10000,
keywords: "AI, chatbot, open-source, copilot, framework",
},
{
id: "2",
title: "EcoHome Essentials",
url: "https://www.ecohomeessentials.com",
headline: "Sustainable Living Made Easy",
description:
"Discover our eco-friendly products that make sustainable living effortless. Shop now for green alternatives!",
budget: 7500,
keywords: "eco-friendly, sustainable, green products, home essentials",
},
{
id: "3",
title: "TechGear Solutions",
url: "https://www.techgearsolutions.com",
headline: "Innovative Tech for the Modern World",
description:
"Find the latest gadgets and tech solutions. Upgrade your life with smart technology today!",
budget: 12000,
keywords: "tech, gadgets, innovative, modern, electronics",
},
{
id: "4",
title: "Global Travels",
url: "https://www.globaltravels.com",
headline: "Travel the World with Confidence",
description:
"Experience bespoke travel packages tailored to your dreams. Luxury, adventure, relaxation—your journey starts here.",
budget: 20000,
keywords: "travel, luxury, adventure, tours, global",
},
{
id: "5",
title: "FreshFit Meals",
url: "https://www.freshfitmeals.com",
headline: "Healthy Eating, Simplified",
description:
"Nutritious, delicious meals delivered to your door. Eating well has never been easier or tastier.",
budget: 5000,
keywords: "healthy, meals, nutrition, delivery, fit",
},
];
由于我们使用 Radix UI 创建可以轻松使用 TailwindCSS 自定义的基础 UI 组件,在 lib 文件夹内创建 utils.ts 文件,并将以下代码片段复制到该文件中。
//👉🏻 lib 文件夹现在包含 3 个文件 - data.ts、type.ts、util.ts
//👇🏻 将下面的代码复制到 "lib/util.ts" 文件中
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function randomId() {
return Math.random().toString(36).substring(2, 15);
}
进入 components 文件夹,并在其内创建三个其他文件夹。
cd components
mkdir app dashboard ui
components/app 文件夹将包含应用内使用的各种组件,而 dashboard 文件夹包含某些元素的 UI 组件。
ui 文件夹使用 Radix UI 创建的多个 UI 元素。将项目仓库内的这些元素复制到该文件夹中。
恭喜!ui 文件夹现在应该包含必要的 UI 元素。现在,我们可以使用它们创建应用内所需的各种组件。
在这里,我将指导你为应用创建用户界面。
首先,导航到 app/page.tsx 文件,并将以下代码片段粘贴到其中。此文件呈现在 components/app 文件夹内声明的 App 组件。
"use client";
import { App } from "@/components/app/App";
export default function DashboardPage() {
return <App />;
}
在 components/app 文件夹内创建 App.tsx、CampaignForm.tsx、MainNav.tsx 和 UserNav.tsx 文件。
cd components/app
touch App.tsx CampaignForm.tsx MainNav.tsx UserNav.tsx
将以下代码片段复制到 App.tsx 文件中。
"use client";
import { DEFAULT_CAMPAIGNS } from "@/lib/data";
import { Campaign } from "@/lib/types";
import { randomId } from "@/lib/utils";
import { Dashboard } from "../dashboard/Dashboard";
import { CampaignForm } from "./CampaignForm";
import { useState } from "react";
import _ from "lodash";
export function App() {
//👇🏻 默认分段
const [segments, setSegments] = useState<string[]>([
"Millennials/Female/Urban",
"Parents/30s/Suburbs",
"Seniors/Female/Rural",
"Professionals/40s/Midwest",
"Gamers/Male",
]);
const [campaigns, setCampaigns] = useState<Campaign[]>(
_.cloneDeep(DEFAULT_CAMPAIGNS)
);
//👇🏻 更新活动列表
function saveCampaign(campaign: Campaign) {
//👇🏻 新创建的活动
if (campaign.id === "") {
campaign.id = randomId();
setCampaigns([campaign, ...campaigns]);
} else {
//👇🏻 现有活动 - 搜索活动并更新活动列表
const index = campaigns.findIndex((c) => c.id === campaign.id);
if (index === -1) {
setCampaigns([...campaigns, campaign]);
} else {
campaigns[index] = campaign;
setCampaigns([...campaigns]);
}
}
}
const [currentCampaign, setCurrentCampaign] = useState<Campaign | undefined>(
undefined
);
return (
<div className='relative'>
<CampaignForm
segments={segments}
currentCampaign={currentCampaign}
setCurrentCampaign={setCurrentCampaign}
saveCampaign={(campaign) => {
if (campaign) {
saveCampaign(campaign);
}
setCurrentCampaign(undefined);
}}
/>
<Dashboard
campaigns={campaigns}
setCurrentCampaign={setCurrentCampaign}
segments={segments}
setSegments={setSegments}
/>
</div>
);
}
在上述代码片段中,我为广告活动创建了一个默认细分受众列表,并对已定义的广告活动列表进行了深拷贝。saveCampaign 函数接收一个广告活动作为参数。如果该广告活动没有 ID,就意味着它是新创建的,因此会将其添加到广告活动列表中。否则,该函数会找到对应的广告活动并更新其属性。Dashboard 和 CampaignForm 组件通过 props 接收细分受众和广告活动。
我为广告活动创建了一个默认细分受众列表,并对已定义的广告活动列表进行了深拷贝。
saveCampaign 函数接收一个广告活动作为参数。如果该广告活动没有 ID,就意味着它是新创建的,因此会将其添加到广告活动列表中。否则,该函数会找到对应的广告活动并更新其属性。
Dashboard 和 CampaignForm 组件通过 props 接收细分受众和广告活动。
Dashboard 组件在仪表盘上显示各种 UI 元素,而 CampaignForm 组件则让用户能够在应用中创建并保存新的广告活动。
你也可以使用 GitHub 仓库中的代码片段更新仪表盘和应用组件。
恭喜!现在你应该已经拥有一个可以正常运行的 Web 应用,用户能够在其中查看和创建新的广告活动。
在下一节中,你将学习如何把 CopilotKit 添加到应用中,以便根据每个广告活动的目标和预算进行分析并做出决策。
在本节中,你将学习如何为应用添加 AI,帮助你分析广告活动并做出最佳决策。
在继续之前,请访问 OpenAI 开发者平台并创建一个新的密钥。
创建一个 .env.local 文件,并将刚刚创建的密钥复制到该文件中。
OPENAI_API_KEY=<YOUR_OPENAI_SECRET_KEY>
OPENAI_MODEL=gpt-4-1106-preview
接下来,你需要为 CopilotKit 创建一个 API 端点。在 Next.js 的 app 文件夹中创建一个 api/copilotkit 文件夹,并在其中创建 route.ts 文件。
cd app
mkdir api && cd api
mkdir copilotkit && cd copilotkit
touch route.ts
将下面的代码片段复制到 route.ts 文件中。CopilotKit 后端会接收用户请求,并使用 OpenAI 模型做出决策。
import { CopilotBackend, OpenAIAdapter } from "@copilotkit/backend";
export const runtime = "edge";
export async function POST(req: Request): Promise<Response> {
const copilotKit = new CopilotBackend({});
const openaiModel = process.env["OPENAI_MODEL"];
return copilotKit.response(req, new OpenAIAdapter({ model: openaiModel }));
}
要将应用连接到这个 API 端点,请按如下方式更新 app/page.tsx 文件:
"use client";
import { App } from "@/components/app/App";
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
export default function DashboardPage() {
return (
<CopilotKit url='/api/copilotkit/'>
<CopilotSidebar
instructions='Help the user create and manage ad campaigns.'
defaultOpen={true}
labels={{
title: "Campaign Manager Copilot",
initial:
"Hello there! I can help you manage your ad campaigns. What campaign would you like to work on?",
}}
clickOutsideToClose={false}
>
<App />
</CopilotSidebar>
</CopilotKit>
);
}
CopilotKit 组件包裹整个应用,并接收一个 url 属性,其中包含指向 API 端点的链接。CopilotSidebar 组件会在应用中添加一个聊天机器人侧边栏面板,让我们能够向 CopilotKit 提供各种指令。
CopilotKit 提供了两个 Hook,让我们能够处理用户请求并接入应用状态:useCopilotAction 和 useMakeCopilotReadable。
useCopilotAction Hook 允许你定义由 CopilotKit 执行的操作。它接收一个包含以下参数的对象:
name——操作名称。
description——操作描述。
parameters——包含所需参数列表的数组。
render——默认的自定义函数或字符串。
handler——由该操作触发的可执行函数。
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}!`);
},
});
useMakeCopilotReadable Hook 会将应用状态提供给 CopilotKit。
import { useMakeCopilotReadable } from "@copilotkit/react-core";
const appState = ...;
useMakeCopilotReadable(JSON.stringify(appState));
CopilotKit 还允许你为用户的提示词提供上下文,使其能够做出恰当且准确的决策。
在项目的 lib 文件夹中添加 guidance.ts 和 script.ts 文件,并将这些指导内容和脚本建议复制到对应文件中,以便 CopilotKit 做出决策。
在 App 组件中,将当前日期、脚本建议和指导内容传递给 CopilotKit。
import { GUIDELINE } from "@/lib/guideline";
import { SCRIPT_SUGGESTION } from "@/lib/script";
import {
useCopilotAction,
useMakeCopilotReadable,
} from "@copilotkit/react-core";
export function App() {
//-- 👉🏻 ...other component functions
//👇🏻 Ground the Copilot with domain-specific knowledge for this use-case: marketing campaigns.
useMakeCopilotReadable(GUIDELINE);
useMakeCopilotReadable(SCRIPT_SUGGESTION);
//👇🏻 Provide the Copilot with the current date.
useMakeCopilotReadable("Today's date is " + new Date().toDateString());
return (
<div className='relative'>
<CampaignForm
segments={segments}
currentCampaign={currentCampaign}
setCurrentCampaign={setCurrentCampaign}
saveCampaign={(campaign) => {
if (campaign) {
saveCampaign(campaign);
}
setCurrentCampaign(undefined);
}}
/>
<Dashboard
campaigns={campaigns}
setCurrentCampaign={setCurrentCampaign}
segments={segments}
setSegments={setSegments}
/>
</div>
);
}
在 App 组件中创建一个 CopilotKit 操作,当用户发出相应指令时,该操作会创建新的广告活动或编辑现有广告活动。
useCopilotAction({
name: "updateCurrentCampaign",
description:
"Edit an existing campaign or create a new one. To update only a part of a campaign, provide the id of the campaign to edit and the new values only.",
parameters: [
{
name: "id",
description:
"The id of the campaign to edit. If empty, a new campaign will be created",
type: "string",
},
{
name: "title",
description: "The title of the campaign",
type: "string",
required: false,
},
{
name: "keywords",
description: "Search keywords for the campaign",
type: "string",
required: false,
},
{
name: "url",
description:
"The URL to link the ad to. Most of the time, the user will provide this value, leave it empty unless asked by the user.",
type: "string",
required: false,
},
{
name: "headline",
description:
"The headline displayed in the ad. This should be a 5-10 words",
type: "string",
required: false,
},
{
name: "description",
description:
"The description displayed in the ad. This should be a short text",
type: "string",
required: false,
},
name: "budget",
description: "活动的预算",
type: "number",
required: false,
},
{
name: "objective",
description: "活动的目标",
type: "string",
enum: [
"brand-awareness",
"lead-generation",
"sales-conversion",
"website-traffic",
"engagement",
],
},
{
name: "bidStrategy",
description: "活动的出价策略",
type: "string",
enum: ["manual-cpc", "cpa", "cpm"],
required: false,
},
{
name: "bidAmount",
description: "活动的出价金额",
type: "number",
required: false,
},
{
name: "segment",
description: "活动的受众细分",
type: "string",
required: false,
enum: segments,
},
],
handler: (campaign) => {
const newValue = _.assign(
_.cloneDeep(currentCampaign),
_.omitBy(campaign, _.isUndefined)
) as Campaign;
setCurrentCampaign(newValue);
},
render: (props) => {
if (props.status === "complete") {
return "活动已成功更新";
} else {
return "正在更新活动";
}
},
});
添加另一个action,用于模拟API调用,让CopilotKit能够从先前创建的活动中检索历史数据。
// 为此组件的Copilot提供检索某些关键词历史成本数据的功能。
// 当CopilotKit需要时将自动调用此功能。
useCopilotAction({
name: "retrieveHistoricalData",
description: "检索某些关键词的历史数据",
parameters: [
{
name: "keywords",
description: "要检索的关键词",
type: "string",
},
{
name: "type",
description: "要为关键词检索的数据类型。",
type: "string",
enum: ["CPM", "CPA", "CPC"],
},
],
handler: async ({ type }) => {
// 模拟一个API调用,根据活动类型(CPM、CPA、CPC)检索某些关键词的历史成本数据
await new Promise((resolve) => setTimeout(resolve, 2000));
function getRandomValue(min: number, max: number) {
return (Math.random() * (max - min) + min).toFixed(2);
}
if (type == "CPM") {
return getRandomValue(0.5, 10);
} else if (type == "CPA") {
return getRandomValue(5, 100);
} else if (type == "CPC") {
return getRandomValue(0.2, 2);
}
},
render: (props) => {
// 自定义的聊天内组件渲染。可根据action的状态渲染不同的组件。
let label = "正在检索历史数据...";
if (props.args.type) {
label = `正在为关键词检索${props.args.type}...`;
}
if (props.status === "complete") {
label = `完成为关键词检索${props.args.type}。`;
}
const done = props.status === "complete";
return (
<div className=''>
<div className=' w-full relative max-w-xs'>
<div className='absolute inset-0 h-full w-full bg-gradient-to-r from-blue-500 to-teal-500 transform scale-[0.80] bg-red-500 rounded-full blur-3xl' />
<div className='relative shadow-xl bg-gray-900 border border-gray-800 px-4 py-8 h-full overflow-hidden rounded-2xl flex flex-col justify-end items-start'>
<h1 className='font-bold text-sm text-white mb-4 relative z-50'>
{label}
</h1>
<p className='font-normal text-base text-teal-200 mb-2 relative z-50 whitespace-pre'>
{props.args.type &&
`历史${props.args.type}数据:${props.result || "..."}`}
</p>
</div>
</div>
</div>
);
},
});
恭喜!你已完成本教程的项目。
CopilotKit是一个令人惊艳的工具,使你能够在数分钟内为产品添加AI智能体。无论你是对AI聊天机器人和助手感兴趣,还是想要自动化复杂任务,CopilotKit都能轻松实现。
如果你需要构建AI产品或将AI工具集成到软件应用中,你应该考虑CopilotKit。
你可以在GitHub上找到本教程的源代码:
https://github.com/CopilotKit/campaign-manager-demo
感谢阅读!