├── .env.local.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── components.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── api
│ │ └── copilotkit
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── app
│ │ ├── App.tsx
│ │ ├── CampaignForm.tsx
│ │ ├── MainNav.tsx
│ │ └── UserNav.tsx
│ ├── dashboard
│ │ ├── ActiveCampaigns.tsx
│ │ ├── Dashboard.tsx
│ │ ├── Overview.tsx
│ │ ├── Segments.tsx
│ │ └── TeamSwitcher.tsx
│ └── ui
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── select.tsx
│ │ └── tabs.tsx
└── lib
│ ├── data.ts
│ ├── guideline.ts
│ ├── script.ts
│ ├── types.ts
│ └── utils.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.local.example:
--------------------------------------------------------------------------------
1 | OPENAI_API_KEY=xxxxxxx
2 | OPENAI_MODEL=gpt-4-1106-preview
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a demo that showcases using CopilotKit to build a PowerPoint like web app.
2 |
3 | ## Deploy with Vercel
4 |
5 | To deploy with Vercel, click the button below:
6 |
7 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FCopilotKit%2Fdemo-campaign-manager&env=NEXT_PUBLIC_COPILOT_CLOUD_API_KEY&project-name=copilotkit-demo-campaign-manager&repository-name=copilotkit-demo-campaign-manager)
8 |
9 | ## Getting Started
10 |
11 | ### 1. install the needed package:
12 |
13 | ```bash
14 | npm i
15 | ```
16 |
17 | ### 2. Set the required environment variables:
18 |
19 | copy `.env.local.example` to `.env.local` and populate the required environment variables.
20 |
21 | > ⚠️ **Important:** Not all users have access to the GPT-4 model yet. If you don't have access, you can use GPT-3 by setting `OPENAI_MODEL` to `gpt-3.5-turbo` in the `.env.local` file.
22 |
23 | ### 3. Run the app
24 |
25 | ```bash
26 | npm run dev
27 | ```
28 |
29 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
30 |
31 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
32 |
33 | ### 4. Use the Copilot
34 |
35 | Create a new campaign, or edit an existing campaign, and use the Copilot assistant to configure the form without requiring domain knowledge.
36 |
37 | ## Zoom in on the CopilotKit code
38 |
39 | 1. Search for `useCopilotReadable` to see where frontend application information is being made accessible to the Copilot engine
40 |
41 | 2. Search for `updateCurrentCampaign` and `retrieveHistoricalData` to see where the frontend application action is made accessible to the Copilot engine.
42 |
43 | ## Learn More
44 |
45 | To learn more about CopilotKit, take a look at the following resources:
46 |
47 | - [CopilotKit Documentation](https://docs.copilotkit.ai/getting-started/quickstart-chatbot) - learn about CopilotKit features and API.
48 | - [GitHub](https://github.com/CopilotKit/CopilotKit) - Check out the CopilotKit GitHub repository.
49 | - [Discord](https://discord.gg/6dffbvGU3D) - Join the CopilotKit Discord community.
50 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "campaign-manager-demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@copilotkit/runtime": "1.0.0-beta.2",
13 | "@copilotkit/react-core": "1.0.0-beta.2",
14 | "@copilotkit/react-textarea": "1.0.0-beta.2",
15 | "@copilotkit/react-ui": "1.0.0-beta.2",
16 | "@copilotkit/shared": "1.0.0-beta.2",
17 | "@heroicons/react": "^2.1.1",
18 | "@radix-ui/react-avatar": "^1.0.4",
19 | "@radix-ui/react-dialog": "^1.0.5",
20 | "@radix-ui/react-dropdown-menu": "^2.0.6",
21 | "@radix-ui/react-icons": "^1.3.0",
22 | "@radix-ui/react-label": "^2.0.2",
23 | "@radix-ui/react-popover": "^1.0.7",
24 | "@radix-ui/react-select": "^2.0.0",
25 | "@radix-ui/react-slot": "^1.0.2",
26 | "@radix-ui/react-tabs": "^1.0.4",
27 | "class-variance-authority": "^0.7.0",
28 | "clsx": "^2.1.0",
29 | "cmdk": "^1.0.0",
30 | "date-fns": "^3.6.0",
31 | "lodash": "^4.17.21",
32 | "next": "14.1.4",
33 | "react": "^18",
34 | "react-day-picker": "^8.10.0",
35 | "react-dom": "^18",
36 | "recharts": "^2.12.3",
37 | "tailwind-merge": "^2.2.2",
38 | "tailwindcss-animate": "^1.0.7"
39 | },
40 | "devDependencies": {
41 | "@types/lodash": "^4.17.0",
42 | "@types/node": "^20",
43 | "@types/react": "^18",
44 | "@types/react-dom": "^18",
45 | "autoprefixer": "^10.0.1",
46 | "eslint": "^8",
47 | "eslint-config-next": "14.1.4",
48 | "postcss": "^8",
49 | "tailwindcss": "^3.3.0",
50 | "typescript": "^5"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/api/copilotkit/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from "next/server";
2 | import {
3 | CopilotRuntime,
4 | OpenAIAdapter,
5 | copilotRuntimeNextJSAppRouterEndpoint,
6 | } from "@copilotkit/runtime";
7 |
8 | export const POST = async (req: NextRequest) => {
9 | const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
10 | runtime: new CopilotRuntime(),
11 | serviceAdapter: new OpenAIAdapter(),
12 | endpoint: req.nextUrl.pathname,
13 | });
14 |
15 | return handleRequest(req);
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CopilotKit/demo-campaign-manager/2e8062603f0cdb54de8aed09cc07987dadc0d0f6/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 |
37 | --copilot-kit-primary-color: var(--primary) !important;
38 | --copilot-kit-contrast-color: var(--secondary) !important;
39 | }
40 | }
41 |
42 | @layer base {
43 | * {
44 | @apply border-border;
45 | }
46 | body {
47 | @apply bg-background text-foreground;
48 | }
49 | }
50 |
51 | .copilotKitWindow {
52 | box-shadow: none !important;
53 | border-left: 1px solid hsl(var(--border)) !important;
54 | }
55 |
56 | .copilotKitHeader {
57 | height: 65px !important;
58 | }
59 |
60 | .copilotKitButton {
61 | background-color: hsl(var(--primary)) !important;
62 | color: hsl(var(--secondary)) !important;
63 | }
64 |
65 | .copilotKitMessage.copilotKitAssistantMessage {
66 | background: hsl(var(--muted)) !important;
67 | }
68 |
69 | .copilotKitMessage.copilotKitUserMessage {
70 | background-color: hsl(var(--primary)) !important;
71 | color: hsl(var(--secondary)) !important;
72 | }
73 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "@copilotkit/react-ui/styles.css";
4 | import "./globals.css";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Create Next App",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | return (
19 |
20 |
{children}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { App } from "@/components/app/App";
3 | import { CopilotKit } from "@copilotkit/react-core";
4 | import { CopilotSidebar } from "@copilotkit/react-ui";
5 |
6 | export default function DashboardPage() {
7 | return (
8 |
13 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/app/App.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { DEFAULT_CAMPAIGNS } from "@/lib/data";
3 | import { Campaign } from "@/lib/types";
4 | import { use, useState } from "react";
5 | import { useCopilotAction, useCopilotReadable } from "@copilotkit/react-core";
6 |
7 | import _ from "lodash";
8 | import { Dashboard } from "../dashboard/Dashboard";
9 | import { CampaignForm } from "./CampaignForm";
10 | import { randomId } from "@/lib/utils";
11 | import { GUIDELINE } from "@/lib/guideline";
12 | import { SCRIPT_SUGGESTION } from "@/lib/script";
13 |
14 | export function App() {
15 | const [segments, setSegments] = useState([
16 | "Millennials/Female/Urban",
17 | "Parents/30s/Suburbs",
18 | "Seniors/Female/Rural",
19 | "Professionals/40s/Midwest",
20 | "Gamers/Male",
21 | ]);
22 |
23 | const [campaigns, setCampaigns] = useState(
24 | _.cloneDeep(DEFAULT_CAMPAIGNS)
25 | );
26 |
27 | function saveCampaign(campaign: Campaign) {
28 | if (campaign.id === "") {
29 | campaign.id = randomId();
30 | setCampaigns([campaign, ...campaigns]);
31 | } else {
32 | const index = campaigns.findIndex((c) => c.id === campaign.id);
33 | if (index === -1) {
34 | setCampaigns([...campaigns, campaign]);
35 | } else {
36 | campaigns[index] = campaign;
37 | setCampaigns([...campaigns]);
38 | }
39 | }
40 | }
41 |
42 | const [currentCampaign, setCurrentCampaign] = useState(
43 | undefined
44 | );
45 |
46 | // Ground the Copilot with domain-specific knowledge for this use-case: marketing campaigns.
47 | useCopilotReadable({ description: "Guideline", value: GUIDELINE });
48 | useCopilotReadable({ description: "Script", value: SCRIPT_SUGGESTION });
49 |
50 | // Provide the Copilot with the current date.
51 | useCopilotReadable({
52 | description: "Current Date",
53 | value: new Date().toDateString(),
54 | });
55 |
56 | // Provide this component's Copilot with the ability to update the current campaign.
57 | //
58 | // This implementation uses a single large function with optional parameters to update the current campaign.
59 | // But you can also use multiple smaller actions to update different parts of the campaign - even one for each field.
60 | // Up to you.
61 | //
62 | // (In the near future we will provide CopilotForm types, which unify useCopilotReadable and useCopilotAction for a given form's values.
63 | // Feel free to ask about this on our Discord: https://discord.gg/t89H6TzmKm).
64 | useCopilotAction({
65 | name: "updateCurrentCampaign",
66 | description:
67 | "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.",
68 | parameters: [
69 | {
70 | name: "id",
71 | description:
72 | "The id of the campaign to edit. If empty, a new campaign will be created",
73 | type: "string",
74 | },
75 | {
76 | name: "title",
77 | description: "The title of the campaign",
78 | type: "string",
79 | required: false,
80 | },
81 | {
82 | name: "keywords",
83 | description: "Search keywords for the campaign",
84 | type: "string",
85 | required: false,
86 | },
87 | {
88 | name: "url",
89 | description:
90 | "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.",
91 | type: "string",
92 | required: false,
93 | },
94 | {
95 | name: "headline",
96 | description:
97 | "The headline displayed in the ad. This should be a 5-10 words",
98 | type: "string",
99 | required: false,
100 | },
101 | {
102 | name: "description",
103 | description:
104 | "The description displayed in the ad. This should be a short text",
105 | type: "string",
106 | required: false,
107 | },
108 |
109 | {
110 | name: "budget",
111 | description: "The budget of the campaign",
112 | type: "number",
113 | required: false,
114 | },
115 | {
116 | name: "objective",
117 | description: "The objective of the campaign",
118 | type: "string",
119 | enum: [
120 | "brand-awareness",
121 | "lead-generation",
122 | "sales-conversion",
123 | "website-traffic",
124 | "engagement",
125 | ],
126 | },
127 |
128 | {
129 | name: "bidStrategy",
130 | description: "The bid strategy of the campaign",
131 | type: "string",
132 | enum: ["manual-cpc", "cpa", "cpm"],
133 | required: false,
134 | },
135 | {
136 | name: "bidAmount",
137 | description: "The bid amount of the campaign",
138 | type: "number",
139 | required: false,
140 | },
141 | {
142 | name: "segment",
143 | description: "The segment of the campaign",
144 | type: "string",
145 | required: false,
146 | enum: segments,
147 | },
148 | ],
149 | handler: (campaign) => {
150 | const newValue = _.assign(
151 | _.cloneDeep(currentCampaign),
152 | _.omitBy(campaign, _.isUndefined)
153 | ) as Campaign;
154 |
155 | setCurrentCampaign(newValue);
156 | },
157 | render: (props) => {
158 | if (props.status === "complete") {
159 | return "Campaign updated successfully";
160 | } else {
161 | return "Updating campaign";
162 | }
163 | },
164 | });
165 |
166 | // Provide this component's Copilot with the ability to retrieve historical cost data for certain keywords.
167 | // Will be called automatically when needed by the Copilot.
168 | useCopilotAction({
169 | name: "retrieveHistoricalData",
170 | description: "Retrieve historical data for certain keywords",
171 | parameters: [
172 | {
173 | name: "keywords",
174 | description: "The keywords to retrieve data for",
175 | type: "string",
176 | },
177 | {
178 | name: "type",
179 | description: "The type of data to retrieve for the keywords.",
180 | type: "string",
181 | enum: ["CPM", "CPA", "CPC"],
182 | },
183 | ],
184 | handler: async ({ type }) => {
185 | // fake an API call that retrieves historical data for cost for certain keywords based on campaign type (CPM, CPA, CPC)
186 | await new Promise((resolve) => setTimeout(resolve, 2000));
187 |
188 | function getRandomValue(min: number, max: number) {
189 | return (Math.random() * (max - min) + min).toFixed(2);
190 | }
191 |
192 | if (type == "CPM") {
193 | return getRandomValue(0.5, 10);
194 | } else if (type == "CPA") {
195 | return getRandomValue(5, 100);
196 | } else if (type == "CPC") {
197 | return getRandomValue(0.2, 2);
198 | }
199 | },
200 | render: (props) => {
201 | // Custom in-chat component rendering. Different components can be rendered based on the status of the action.
202 | let label = "Retrieving historical data ...";
203 | if (props.args.type) {
204 | label = `Retrieving ${props.args.type} for keywords ...`;
205 | }
206 | if (props.status === "complete") {
207 | label = `Done retrieving ${props.args.type} for keywords.`;
208 | }
209 |
210 | const done = props.status === "complete";
211 | return (
212 |
213 |
214 |
215 |
216 |
217 | {label}
218 |
219 |
220 | {props.args.type &&
221 | `Historical ${props.args.type}: ${props.result || "..."}`}
222 |
223 |
224 |
225 |
226 | );
227 | },
228 | });
229 |
230 | return (
231 |
232 | {
237 | if (campaign) {
238 | saveCampaign(campaign);
239 | }
240 | setCurrentCampaign(undefined);
241 | }}
242 | />
243 |
249 |
250 | );
251 | }
252 |
--------------------------------------------------------------------------------
/src/components/app/CampaignForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Campaign } from "@/lib/types";
3 | import { Button } from "@/components/ui/button";
4 | import { Label } from "@/components/ui/label";
5 | import { Input } from "@/components/ui/input";
6 | import clsx from "clsx";
7 | import {
8 | Select,
9 | SelectContent,
10 | SelectItem,
11 | SelectTrigger,
12 | SelectValue,
13 | } from "@/components/ui/select";
14 | import { useCopilotReadable } from "@copilotkit/react-core";
15 |
16 | interface CampaignFormProps {
17 | currentCampaign?: Campaign;
18 | setCurrentCampaign: (campaign?: Campaign) => void;
19 | segments: string[];
20 | saveCampaign: (campaign?: Campaign) => void;
21 | }
22 |
23 | export function CampaignForm({
24 | currentCampaign,
25 | setCurrentCampaign,
26 | saveCampaign,
27 | segments,
28 | }: CampaignFormProps) {
29 | useCopilotReadable({
30 | description: "Current Campaign",
31 | value: currentCampaign,
32 | });
33 |
34 | if (!currentCampaign) return null;
35 | return (
36 |
43 |
44 |
45 | {currentCampaign.id == "" ? "New" : "Edit"} Campaign
46 |
47 |
48 |
49 |
Campaign Information
50 |
51 |
58 |
72 |
73 |
74 | [s, s]))}
81 | />
82 |
83 |
84 |
85 |
Budget & Bidding
86 |
87 |
94 |
95 |
96 |
108 |
115 |
116 |
117 |
118 |
Ad Copy
119 |
120 |
127 |
134 |
135 |
136 |
143 |
150 |
151 |
152 |
153 |
154 |
161 |
162 |
163 |
164 |
165 | );
166 | }
167 |
168 | interface TextInputProps {
169 | id: string;
170 | label: string;
171 | className?: string;
172 | campaign: Campaign;
173 | setCampaign: (campaign: Campaign) => void;
174 | }
175 |
176 | function TextInput({
177 | id,
178 | label,
179 | className,
180 | campaign,
181 | setCampaign,
182 | }: TextInputProps) {
183 | return (
184 |
185 |
188 |
194 | setCampaign({
195 | ...campaign,
196 | [id]: e.target.value,
197 | })
198 | }
199 | value={(campaign as any)[id] || ""}
200 | />
201 |
202 | );
203 | }
204 |
205 | interface DropdownProps {
206 | id: string;
207 | label: string;
208 | className?: string;
209 | campaign: Campaign;
210 | setCampaign: (campaign: Campaign) => void;
211 | items: { [key: string]: string };
212 | }
213 |
214 | function Dropdown({
215 | id,
216 | label,
217 | className,
218 | campaign,
219 | setCampaign,
220 | items,
221 | }: DropdownProps) {
222 | return (
223 |
224 |
227 |
242 |
243 | );
244 | }
245 |
--------------------------------------------------------------------------------
/src/components/app/MainNav.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export function MainNav({
6 | className,
7 | ...props
8 | }: React.HTMLAttributes) {
9 | return (
10 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/app/UserNav.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuGroup,
7 | DropdownMenuItem,
8 | DropdownMenuLabel,
9 | DropdownMenuSeparator,
10 | DropdownMenuShortcut,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu";
13 | import { Input } from "@/components/ui/input";
14 |
15 | export function UserNav() {
16 | return (
17 | <>
18 |
19 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
Markus Ecker
38 |
39 | markus@copilotkit.ai
40 |
41 |
42 |
43 |
44 |
45 |
46 | Profile
47 | ⇧⌘P
48 |
49 |
50 | Billing
51 | ⌘B
52 |
53 |
54 | Settings
55 | ⌘S
56 |
57 | New Team
58 |
59 |
60 |
61 | Log out
62 | ⇧⌘Q
63 |
64 |
65 |
66 | >
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/dashboard/ActiveCampaigns.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
2 | import { Campaign } from "@/lib/types";
3 |
4 | interface ActiveCampaignsProps {
5 | campaigns: Campaign[];
6 | setCurrentCampaign: (campaign: Campaign) => void;
7 | }
8 |
9 | export function ActiveCampaigns({
10 | campaigns,
11 | setCurrentCampaign,
12 | }: ActiveCampaignsProps) {
13 | return (
14 |
15 | {campaigns.map((campaign) => (
16 |
setCurrentCampaign(campaign)}
20 | />
21 | ))}
22 |
23 | );
24 | }
25 |
26 | interface ActiveCampaignProps {
27 | campaign: Campaign;
28 | onClick: (campaign: Campaign) => void;
29 | }
30 |
31 | function ActiveCampaign({ campaign, onClick }: ActiveCampaignProps) {
32 | const titleInitials = (campaign.title || "")
33 | .split(" ")
34 | .map((word) => (word.length > 0 ? word[0] : ""))
35 | .slice(0, 2)
36 | .join("")
37 | .toUpperCase();
38 | const budget = new Intl.NumberFormat("en-US", {
39 | style: "currency",
40 | currency: "USD",
41 | }).format(Math.abs(campaign.budget));
42 |
43 | return (
44 | {
47 | onClick(campaign);
48 | }}
49 | >
50 |
51 |
52 | {titleInitials}
53 |
54 |
55 |
56 |
{campaign.title}
57 |
61 | {budget}
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { MainNav } from "@/components/app/MainNav";
2 | import { UserNav } from "@/components/app/UserNav";
3 | import { ActiveCampaigns } from "@/components/dashboard/ActiveCampaigns";
4 | import { Overview } from "@/components/dashboard/Overview";
5 | import TeamSwitcher from "@/components/dashboard/TeamSwitcher";
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | Card,
9 | CardContent,
10 | CardDescription,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card";
14 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
15 | import { Campaign } from "@/lib/types";
16 | import { PlusCircleIcon } from "@heroicons/react/24/outline";
17 | import { Pencil1Icon } from "@radix-ui/react-icons";
18 | import { useState } from "react";
19 | import SegmentsManager from "./Segments";
20 |
21 | interface DashboardProps {
22 | campaigns: Campaign[];
23 | setCurrentCampaign: (campaign: Campaign) => void;
24 | segments: string[];
25 | setSegments: (segments: string[]) => void;
26 | }
27 |
28 | export function Dashboard({
29 | campaigns,
30 | setCurrentCampaign,
31 | segments,
32 | setSegments,
33 | }: DashboardProps) {
34 | const [isEditingSegments, setIsEditingSegments] = useState(false);
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Dashboard
51 |
52 |
60 |
61 |
76 |
77 |
78 |
79 |
80 | Overview
81 |
82 | Analytics
83 |
84 |
85 | Reports
86 |
87 |
88 | Notifications
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | Total Cost
97 |
98 |
110 |
111 |
112 | $45,231.89
113 |
114 | +20.1% from last month
115 |
116 |
117 |
118 |
119 |
120 |
121 | Impressions
122 |
123 |
137 |
138 |
139 | 38M
140 |
141 | +36.2% from last month
142 |
143 |
144 |
145 |
146 |
147 |
148 | Clicks
149 |
150 |
162 |
163 |
164 | 2.8M
165 |
166 | +40.8% from last month
167 |
168 |
169 |
170 |
171 |
172 |
173 | Conversions
174 |
175 |
188 |
189 |
190 | 199K
191 |
192 | +31.2% from last month
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 | Spendings Overview
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | Active Campaigns
209 |
210 | You have {campaigns.length} active campaign
211 | {campaigns.length != 1 && "s"}.
212 |
213 |
214 |
215 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | {isEditingSegments && (
227 | {
231 | setIsEditingSegments(false);
232 | }}
233 | />
234 | )}
235 | >
236 | );
237 | }
238 |
--------------------------------------------------------------------------------
/src/components/dashboard/Overview.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
4 |
5 | // Work around React deprecation warning
6 | const XAxisDefaultProps = XAxis.defaultProps;
7 | const YAxisDefaultProps = YAxis.defaultProps;
8 | delete XAxis.defaultProps;
9 | delete YAxis.defaultProps;
10 |
11 | const MAX_VALUE = 10000;
12 | const MIN_VALUE = 1000;
13 |
14 | const data = [
15 | {
16 | name: "Jan",
17 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
18 | },
19 | {
20 | name: "Feb",
21 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
22 | },
23 | {
24 | name: "Mar",
25 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
26 | },
27 | {
28 | name: "Apr",
29 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
30 | },
31 | {
32 | name: "May",
33 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
34 | },
35 | {
36 | name: "Jun",
37 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
38 | },
39 | {
40 | name: "Jul",
41 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
42 | },
43 | {
44 | name: "Aug",
45 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
46 | },
47 | {
48 | name: "Sep",
49 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
50 | },
51 | {
52 | name: "Oct",
53 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
54 | },
55 | {
56 | name: "Nov",
57 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
58 | },
59 | {
60 | name: "Dec",
61 | total: Math.floor(Math.random() * MAX_VALUE) + MIN_VALUE,
62 | },
63 | ];
64 |
65 | export function Overview() {
66 | return (
67 |
68 |
69 |
77 | `$${value}`}
84 | />
85 |
91 |
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/dashboard/Segments.tsx:
--------------------------------------------------------------------------------
1 | import { TrashIcon, XCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
2 | import React, { useState } from "react";
3 | import { Input } from "../ui/input";
4 | import { Button } from "../ui/button";
5 |
6 | interface SegmentsManagerProps {
7 | segments: string[];
8 | setSegments: (segments: string[]) => void;
9 | onClose: () => void;
10 | }
11 |
12 | const SegmentsManager = ({
13 | segments,
14 | setSegments,
15 | onClose,
16 | }: SegmentsManagerProps) => {
17 | const [newSegment, setNewSegment] = useState("");
18 |
19 | const addSegment = () => {
20 | if (newSegment) {
21 | setSegments([...segments, newSegment]);
22 | setNewSegment(""); // Reset the input
23 | }
24 | };
25 |
26 | const removeSegment = (index: number) => {
27 | setSegments(segments.filter((_, i) => i !== index));
28 | };
29 |
30 | return (
31 |
38 |
39 |
40 |
41 | Edit Customer Segments
42 |
43 |
46 |
47 |
48 |
49 | setNewSegment(e.target.value)}
54 | placeholder="Enter a new segment"
55 | />
56 |
57 |
58 |
59 |
60 | {segments.map((segment, index) => (
61 | -
62 | {segment}{" "}
63 |
66 |
67 | ))}
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default SegmentsManager;
76 |
--------------------------------------------------------------------------------
/src/components/dashboard/TeamSwitcher.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | PlusCircledIcon,
8 | } from "@radix-ui/react-icons";
9 |
10 | import { cn } from "@/lib/utils";
11 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
12 | import { Button } from "@/components/ui/button";
13 | import {
14 | Command,
15 | CommandEmpty,
16 | CommandGroup,
17 | CommandInput,
18 | CommandItem,
19 | CommandList,
20 | CommandSeparator,
21 | } from "@/components/ui/command";
22 | import {
23 | Popover,
24 | PopoverContent,
25 | PopoverTrigger,
26 | } from "@/components/ui/popover";
27 |
28 | const groups = [
29 | {
30 | label: "Personal Account",
31 | teams: [
32 | {
33 | label: "Markus Ecker",
34 | value: "personal",
35 | },
36 | ],
37 | },
38 | {
39 | label: "Teams",
40 | teams: [
41 | {
42 | label: "PilotDesk",
43 | value: "pilotdesk",
44 | },
45 | {
46 | label: "TawKit Inc.",
47 | value: "tawkit-inc",
48 | },
49 | ],
50 | },
51 | ];
52 |
53 | type PopoverTriggerProps = React.ComponentPropsWithoutRef<
54 | typeof PopoverTrigger
55 | >;
56 |
57 | interface TeamSwitcherProps extends PopoverTriggerProps {}
58 |
59 | export default function TeamSwitcher({ className }: TeamSwitcherProps) {
60 | const [open, setOpen] = React.useState(false);
61 | const selectedTeam = groups[0].teams[0];
62 |
63 | return (
64 |
65 |
66 |
84 |
85 |
86 |
87 |
88 |
89 | No team found.
90 | {groups.map((group) => (
91 |
92 | {group.teams.map((team) => (
93 |
94 |
95 |
100 | ME
101 |
102 | {team.label}
103 |
111 |
112 | ))}
113 |
114 | ))}
115 |
116 |
117 |
118 |
119 |
120 |
121 | Create Team
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
43 | : "[&:has([aria-selected])]:rounded-md"
44 | ),
45 | day: cn(
46 | buttonVariants({ variant: "ghost" }),
47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
48 | ),
49 | day_range_start: "day-range-start",
50 | day_range_end: "day-range-end",
51 | day_selected:
52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
53 | day_today: "bg-accent text-accent-foreground",
54 | day_outside:
55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
56 | day_disabled: "text-muted-foreground opacity-50",
57 | day_range_middle:
58 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
59 | day_hidden: "invisible",
60 | ...classNames,
61 | }}
62 | components={{
63 | IconLeft: ({ ...props }) => ,
64 | IconRight: ({ ...props }) => ,
65 | }}
66 | {...props}
67 | />
68 | )
69 | }
70 | Calendar.displayName = "Calendar"
71 |
72 | export { Calendar }
73 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
6 | import { Command as CommandPrimitive } from "cmdk"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { Cross2Icon } from "@radix-ui/react-icons";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ))
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
34 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/lib/data.ts:
--------------------------------------------------------------------------------
1 | import { Campaign } from "./types";
2 |
3 | export let DEFAULT_CAMPAIGNS: Campaign[] = [
4 | {
5 | id: "1",
6 | title: "CopilotKit",
7 | url: "https://www.copilotkit.ai",
8 | headline: "Copilot Kit - The Open-Source Copilot Framework",
9 | description:
10 | "Build, deploy, and operate fully custom AI Copilots. In-app AI chatbots, AI agents, AI Textareas and more.",
11 | budget: 10000,
12 | keywords: "AI, chatbot, open-source, copilot, framework",
13 | },
14 | {
15 | id: "2",
16 | title: "EcoHome Essentials",
17 | url: "https://www.ecohomeessentials.com",
18 | headline: "Sustainable Living Made Easy",
19 | description:
20 | "Discover our eco-friendly products that make sustainable living effortless. Shop now for green alternatives!",
21 | budget: 7500,
22 | keywords: "eco-friendly, sustainable, green products, home essentials",
23 | },
24 | {
25 | id: "3",
26 | title: "TechGear Solutions",
27 | url: "https://www.techgearsolutions.com",
28 | headline: "Innovative Tech for the Modern World",
29 | description:
30 | "Find the latest gadgets and tech solutions. Upgrade your life with smart technology today!",
31 | budget: 12000,
32 | keywords: "tech, gadgets, innovative, modern, electronics",
33 | },
34 | {
35 | id: "4",
36 | title: "Global Travels",
37 | url: "https://www.globaltravels.com",
38 | headline: "Travel the World with Confidence",
39 | description:
40 | "Experience bespoke travel packages tailored to your dreams. Luxury, adventure, relaxation—your journey starts here.",
41 | budget: 20000,
42 | keywords: "travel, luxury, adventure, tours, global",
43 | },
44 | {
45 | id: "5",
46 | title: "FreshFit Meals",
47 | url: "https://www.freshfitmeals.com",
48 | headline: "Healthy Eating, Simplified",
49 | description:
50 | "Nutritious, delicious meals delivered to your door. Eating well has never been easier or tastier.",
51 | budget: 5000,
52 | keywords: "healthy, meals, nutrition, delivery, fit",
53 | },
54 | ];
55 |
--------------------------------------------------------------------------------
/src/lib/guideline.ts:
--------------------------------------------------------------------------------
1 | export const GUIDELINE: string = `
2 | # Ad Campaign Guidelines Document
3 |
4 | ## Campaign Objectives
5 |
6 | - **Brand Awareness**: Choose when introducing a new product or reaching a new market. The focus is on maximizing exposure.
7 | - **Lead Generation**: Suitable for building a customer database, often for services or B2B products.
8 | - **Sales Conversion**: Prioritize when the primary goal is to drive immediate online sales.
9 | - **Website Traffic**: Select to increase the number of visitors to a particular website or page.
10 | - **Engagement**: Ideal for content-driven campaigns aimed at interaction (likes, comments, shares).
11 |
12 | ## Bid Strategies
13 |
14 | - **Manual CPC (Cost Per Click)**: Use when you want control over the bid amounts for clicks.
15 | - **CPA (Cost Per Acquisition)**: Best for campaigns focused on conversions like sales or sign-ups.
16 | - **CPM (Cost Per Thousand Impressions)**: Recommended for brand awareness campaigns where impressions are more valuable than clicks.
17 |
18 | ## Scenario-Specific Actions
19 |
20 | ### Scenario: Launching a New Product
21 | - **Objective:** Brand Awareness
22 | - **Bid Strategy:** CPM
23 | - **Calculations:**
24 | - Determine your target reach and average industry CPM rates.
25 |
26 | ### Scenario: Maximizing Online Sales During a Holiday
27 | - **Objective:** Sales Conversion
28 | - **Bid Strategy:** CPA
29 | - **Calculations:**
30 | - Estimate the number of sales you aim
31 |
32 | ### Scenario: Growing the Subscriber List
33 | - **Objective:** Lead Generation
34 | - **Bid Strategy:** CPA or Manual CPC
35 | - **Calculations:**
36 | - Decide on Bid Amount based on historical data and the competitive landscape.
37 |
38 | ### Scenario: Increasing Blog Readership
39 | - **Objective:** Website Traffic
40 | - **Bid Strategy:** Manual CPC
41 | - **Calculations:**
42 | - Set a Bid Amount slightly higher than the average CPC to remain competitive.
43 |
44 | ### Scenario: Engaging the Community with Content
45 | - **Objective:** Engagement
46 | - **Bid Strategy:** CPM or Manual CPC (if specific interactions are more valuable)
47 | - **Calculations:**
48 | - Determine Bid Amount based on whether you're paying per interaction (CPC) or per impression (CPM).
49 |
50 | ## Prohibited content
51 |
52 | THE FOLLOWING CONTENT IS PROHIBITED FROM ADVERTISING ON OUR PLATFORM.
53 | DO NOT CREATE CAMPAIGNS THAT PROMOTE OR SELL THE FOLLOWING, INSTEAD INFORM THE USER OF THE VIOLATION.
54 |
55 | ### Counterfeit goods
56 | Campaign manager prohibits the sale or promotion for sale of counterfeit goods. Counterfeit goods contain a trademark or logo that is identical to or substantially indistinguishable from the trademark of another. They mimic the brand features of the product in an attempt to pass themselves off as a genuine product of the brand owner. This policy applies to the content of your ad and your website or app.
57 |
58 | ### Dangerous products or services
59 | We want to help keep people safe both online and offline, so we don't allow the promotion of some products or services that cause damage, harm, or injury.
60 | Examples of dangerous content: Recreational drugs (chemical or herbal); psychoactive substances; equipment to facilitate drug use; weapons, ammunition, explosive materials and fireworks; instructions for making explosives or other harmful products; tobacco products
61 |
62 | ### Enabling dishonest behavior
63 | We value honesty and fairness, so we don't allow the promotion of products or services that are designed to enable dishonest behavior.
64 | Examples of products or services that enable dishonest behavior: Hacking software or instructions; services designed to artificially inflate ad or website traffic; fake documents; academic cheating services
65 |
66 | ### Inappropriate content
67 | We value diversity and respect for others, and we strive to avoid offending users, so we don't allow ads or destinations that display shocking content or promote hatred, intolerance, discrimination, or violence.
68 |
69 | Examples of inappropriate or offensive content: bullying or intimidation of an individual or group, racial discrimination, hate group paraphernalia, graphic crime scene or accident images, cruelty to animals, murder, self-harm, extortion or blackmail, sale or trade of endangered species, ads using profane language
70 | `;
71 |
--------------------------------------------------------------------------------
/src/lib/script.ts:
--------------------------------------------------------------------------------
1 | export const SCRIPT_SUGGESTION = `
2 | WHENEVER THE USER WANTS TO CREATE A NEW CAMPAIGN, THE ASSISTANT MUST ASK A SERIES OF QUESTIONS ONE BY ONE TO GATHER THE NECESSARY INFORMATION TO SET UP THE CAMPAIGN SUCCESSFULLY.
3 |
4 | ONCE YOU HAVE THE REQUIRED INFORMATION, USE YOUR BEST GUESS FOR THE REST OF THE FIELDS.
5 |
6 | **Assistant:** “To begin with, could you please share the main goal of your advertising campaign? Are you looking to enhance brand recognition, generate leads, boost sales, increase traffic to your website, or engage with your audience in a more meaningful way?”
7 |
8 | - This will determine the **Campaign Objective** and guide the choice in **Bid Strategy**.
9 |
10 | **Assistant:** “What budget have you allocated for this campaign, and do you have a specific time frame in mind for how long you'd like the campaign to run?”
11 |
12 | - The answer will help the agent set the **Total Budget**, and possibly even influence the **Bid Amount**.
13 |
14 | **Assistant:** “Could you describe your ideal customer? What are their interests, demographics, and online behaviors?”
15 |
16 | - Understanding the target audience will assist in selecting appropriate **Keywords**, crafting the **Headline** and **Description** for the ad copy, and further refine the **Campaign Objective** and **Bid Strategy**.
17 | - The user has defined their own list of **Segments** that can be used to further target the audience. It's important to set the right segment for the campaign if it can be inferred from the user's response.
18 |
19 |
20 | WHEN FILLING THE FORM, ALSO RETRIEVE THE CORRECT CPC, CPM OR CPA FROM HISTORICAL DATA TO SET THE BID AMOUNT.
21 | `;
22 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export interface Campaign {
2 | id: string;
3 | objective?:
4 | | "brand-awareness"
5 | | "lead-generation"
6 | | "sales-conversion"
7 | | "website-traffic"
8 | | "engagement";
9 | title: string;
10 | keywords: string;
11 | url: string;
12 | headline: string;
13 | description: string;
14 | budget: number;
15 | bidStrategy?: "manual-cpc" | "cpa" | "cpm";
16 | bidAmount?: number;
17 | segment?: string;
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function randomId() {
9 | return Math.random().toString(36).substring(2, 15);
10 | }
11 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------