feat(frontend): Onboarding flow UI (#9485)

### Changes 🏗️
This PR adds interactive UI for the onboarding flow, without any
connection to the backend.
Visit `/onboarding` to see it!

- Add Onboarding pages to `app/onboarding/`
- Add Onboarding components to `components/onboarding`

Note:
- Backend isn't connected, so the agents won't run and state isn't
preserved
- Onboarding state is lost on refresh

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] ...
This commit is contained in:
Krzysztof Czerwinski
2025-02-21 17:07:27 +01:00
committed by GitHub
parent 70d095ba98
commit 0b992223df
42 changed files with 1428 additions and 57 deletions

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="AUTOgpt_logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 2000 2000" style="enable-background:new 0 0 2000 2000;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_00000035521710159047349970000013067780781047948184_);}
.st2{fill:url(#SVGID_00000151539662336123527000000001111225930080654741_);}
.st3{fill:url(#SVGID_00000173879856226243399160000002461592630409682563_);}
.st4{fill:url(#SVGID_00000048473209507965140620000018306312736405929911_);}
.st5{fill:#669CF6;}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="14753.7793" y1="16374.9854" x2="14753.7793" y2="19678.4609" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#000030"/>
<stop offset="1" style="stop-color:#9900FF"/>
</linearGradient>
<path class="st0" d="M1009.2,1166.2v183.2c0,13.5-11,24.5-24.5,24.5c-14.6,0-26.6-9.2-26.6-24.5V1076c0-58,47.1-105.1,105.1-105.1
c58.1,0,105.1,47.1,105.1,105.1c0,58.1-47.1,105.1-105.1,105.1C1043.5,1181.1,1025,1175.7,1009.2,1166.2L1009.2,1166.2z
M1030.7,1043.6c17.9,0,32.5,14.5,32.5,32.4s-14.5,32.5-32.5,32.5c-3.3,0-6.6-0.5-9.6-1.4c9.5,12.9,24.8,21.2,42.1,21.2
c28.9,0,52.3-23.4,52.3-52.3s-23.4-52.3-52.3-52.3c-17.2,0-32.5,8.4-42.1,21.2C1024.2,1044.1,1027.4,1043.6,1030.7,1043.6
L1030.7,1043.6z"/>
<linearGradient id="SVGID_00000043446088280357152930000008550688705684522905_" gradientUnits="userSpaceOnUse" x1="10683.2227" y1="16371.9873" x2="10683.2227" y2="19679.123" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#000030"/>
<stop offset="1" style="stop-color:#4285F4"/>
</linearGradient>
<path style="fill:url(#SVGID_00000043446088280357152930000008550688705684522905_);" d="M877.9,1166.2v117.7
c0,10.7-4.4,20.4-11.4,27.5c-15.5,15.4-44.2,15.4-59.6,0c-20.3-20.3-1-54.7-37.4-91.1c-35.3-35.3-96.6-35.3-131.9,0
c-16.3,16.3-26.4,38.8-26.4,63.6c0,13.5,11,24.5,24.5,24.5c14.6,0,26.6-9.2,26.6-24.5c0-10.7,4.4-20.4,11.4-27.4
c15.5-15.4,44.2-15.4,59.6,0c22.1,22.1-0.2,53.4,37.4,91c35.3,35.3,96.6,35.3,131.9,0c16.3-16.3,26.4-38.8,26.4-63.6v-207.2v-0.7
c0-58-47.1-105.1-105.1-105.1S718.8,1018,718.8,1076c0,58.1,47.1,105.1,105.1,105.1C843.7,1181.2,862.1,1175.7,877.9,1166.2z
M823.9,1128.3c-17.2,0-32.5-8.4-42.1-21.2c3,0.9,6.2,1.4,9.6,1.4c17.9,0,32.5-14.5,32.5-32.5c0-17.9-14.5-32.4-32.5-32.4
c-3.3,0-6.6,0.5-9.6,1.4c9.5-12.9,24.8-21.2,42.1-21.2c28.8,0,52.3,23.4,52.3,52.3S852.8,1128.3,823.9,1128.3L823.9,1128.3
L823.9,1128.3z"/>
<linearGradient id="SVGID_00000115513728909957486860000012725271435380503185_" gradientUnits="userSpaceOnUse" x1="16543.2012" y1="12415.3223" x2="10832.0879" y2="9706.5195" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#4285F4"/>
<stop offset="1" style="stop-color:#9900FF"/>
</linearGradient>
<path style="fill:url(#SVGID_00000115513728909957486860000012725271435380503185_);" d="M1244.9,868c0-64.6-25.7-126-71.3-171.6
s-107-71.3-171.6-71.3s-126,25.7-171.6,71.3C784.7,742,759,803.4,759,868v11c0,14.1,11.4,25.5,25.5,25.5
c14.1,0,25.5-11.4,25.5-25.5v-11c0-51.1,20.4-99.5,56.4-135.6s84.5-56.5,135.6-56.5s99.5,20.4,135.6,56.5
c36.1,36.1,56.4,84.5,56.4,135.6c0,14.1,11.4,25.5,25.5,25.5S1244.9,882.1,1244.9,868z"/>
<linearGradient id="SVGID_00000118397539648260588790000004092116245252367524_" gradientUnits="userSpaceOnUse" x1="17851.2109" y1="14970.332" x2="17851.2109" y2="20014.2305" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#000030"/>
<stop offset="1" style="stop-color:#4285F4"/>
</linearGradient>
<path style="fill:url(#SVGID_00000118397539648260588790000004092116245252367524_);" d="M1244.9,944.9v31.4h41
c13.7,0,24.8,11.1,24.8,24.8c0,14.3-9.8,26.3-24.8,26.3h-41v257.5c0,10.7,4.4,20.4,11.4,27.4c15.4,15.5,44.1,15.5,59.6,0
c7-7,11.4-16.8,11.4-27.4v-8.6c0-15.3,12-24.5,26.6-24.5c13.5,0,24.5,11,24.5,24.5v8.6c0,24.8-10.1,47.3-26.4,63.6
c-35.3,35.3-96.4,35.3-131.7,0c-16.3-16.3-26.4-38.8-26.4-63.6v-340c0-14.8,11.7-24.8,26.1-24.8
C1233.7,920.1,1244.9,931.3,1244.9,944.9L1244.9,944.9z"/>
<linearGradient id="SVGID_00000009586294747254918480000014299822675664607379_" gradientUnits="userSpaceOnUse" x1="15189.209" y1="17240.8633" x2="15384.8818" y2="19057.8242" gradientTransform="matrix(7.200000e-02 0 0 7.200000e-02 0.928 1.072)">
<stop offset="0" style="stop-color:#4285F4"/>
<stop offset="1" style="stop-color:#9900FF"/>
</linearGradient>
<path style="fill:url(#SVGID_00000009586294747254918480000014299822675664607379_);" d="M1127.2,1349.4c0,13.5-11,24.5-24.5,24.5
c-14.6,0-26.6-9.2-26.6-24.5c0-74.8,0-8.3,0-83.2c0-13.5,11-24.5,24.5-24.5c14.6,0,26.6,9.2,26.6,24.5
C1127.2,1341.1,1127.2,1274.6,1127.2,1349.4z"/>
<circle class="st5" cx="1352.9" cy="1203.6" r="26.2"/>
<circle class="st5" cx="635.7" cy="1347.7" r="26.2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -12,6 +12,7 @@ import { Toaster } from "@/components/ui/toaster";
import { IconType } from "@/components/ui/icons";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { headers } from "next/headers";
// Fonts
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
@@ -31,6 +32,10 @@ export default async function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const pathname = headers().get("x-current-path");
const isOnboarding = pathname?.startsWith("/onboarding");
console.log("pathname:", pathname);
return (
<html
lang="en"
@@ -45,63 +50,65 @@ export default async function RootLayout({
disableTransitionOnChange
>
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
<Navbar
links={[
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/monitoring",
},
{
name: "Build",
href: "/build",
},
]}
menuItemGroups={[
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
]}
/>
{!isOnboarding && (
<Navbar
links={[
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/monitoring",
},
{
name: "Build",
href: "/build",
},
]}
menuItemGroups={[
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
]}
/>
)}
<main className="w-full flex-grow">{children}</main>
<TallyPopupSimple />
</div>

View File

@@ -0,0 +1,33 @@
"use client";
import { OnboardingText } from "@/components/onboarding/OnboardingText";
import { useOnboarding } from "../layout";
import OnboardingButton from "@/components/onboarding/OnboardingButton";
import Image from "next/image";
export default function Page() {
const {} = useOnboarding(1);
return (
<>
<Image
src="/gpt_dark_RGB.svg"
alt="GPT Dark Logo"
className="-mb-2"
width={300}
height={300}
/>
<OnboardingText className="mb-3" variant="header" center>
Welcome to AutoGPT
</OnboardingText>
<OnboardingText className="mb-12" center>
Think of AutoGPT as your digital teammate, working intelligently to
<br />
complete tasks based on your directions. Let&apos;s learn a bit about
you to
<br />
tailor your experience.
</OnboardingText>
<OnboardingButton href="/onboarding/2-reason">Continue</OnboardingButton>
</>
);
}

View File

@@ -0,0 +1,72 @@
"use client";
import OnboardingButton from "@/components/onboarding/OnboardingButton";
import { useOnboarding } from "../layout";
import {
OnboardingFooter,
OnboardingHeader,
OnboardingStep,
} from "@/components/onboarding/OnboardingStep";
import { OnboardingText } from "@/components/onboarding/OnboardingText";
import OnboardingList from "@/components/onboarding/OnboardingList";
const reasons = [
{
label: "Content & Marketing",
text: "Content creation, social media management, blogging, creative writing",
id: "content_marketing",
},
{
label: "Business & Workflow Automation",
text: "Operations, task management, productivity",
id: "business_workflow_automation",
},
{
label: "Data & Research",
text: "Data analysis, insights, research, financial operation",
id: "data_research",
},
{
label: "AI & Innovation",
text: "AI experimentation, automation testing, advanced AI applications",
id: "ai_innovation",
},
{
label: "Personal productivity",
text: "Automating daily tasks, organizing information, personal workflows",
id: "personal_productivity",
},
];
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
return !str || str.trim().length === 0;
}
export default function Page() {
const { state, setState } = useOnboarding(2);
return (
<OnboardingStep>
<OnboardingHeader backHref={"/onboarding/1-welcome"}>
<OnboardingText className="mt-4" variant="header" center>
What&apos;s your main reason for using AutoGPT?
</OnboardingText>
<OnboardingText className="mt-1" center>
Select the option that best matches your needs
</OnboardingText>
</OnboardingHeader>
<OnboardingList
elements={reasons}
selectedId={state.usageReason}
onSelect={(usageReason) => setState({ usageReason })}
/>
<OnboardingFooter>
<OnboardingButton
href="/onboarding/3-services"
disabled={isEmptyOrWhitespace(state.usageReason)}
>
Next
</OnboardingButton>
</OnboardingFooter>
</OnboardingStep>
);
}

View File

@@ -0,0 +1,178 @@
"use client";
import OnboardingButton from "@/components/onboarding/OnboardingButton";
import {
OnboardingStep,
OnboardingHeader,
OnboardingFooter,
} from "@/components/onboarding/OnboardingStep";
import { OnboardingText } from "@/components/onboarding/OnboardingText";
import { useOnboarding } from "../layout";
import { OnboardingGrid } from "@/components/onboarding/OnboardingGrid";
import { useCallback } from "react";
import OnboardingInput from "@/components/onboarding/OnboardingInput";
const services = [
{
name: "D-ID",
text: "Generate AI-powered avatars and videos for dynamic content creation.",
icon: "/integrations/d-id.png",
},
{
name: "Discord",
text: "A chat platform for communities and teams, supporting text, voice, and video.",
icon: "/integrations/discord.png",
},
{
name: "GitHub",
text: "AutoGPT can track issues, manage repos, and automate workflows with GitHub.",
icon: "/integrations/github.png",
},
{
name: "Google Workspace",
text: "Automate emails, calendar events, and document management in AutoGPT with Google Workspace.",
icon: "/integrations/google.png",
},
{
name: "Google Maps",
text: "Fetch locations, directions, and real-time geodata for navigation.",
icon: "/integrations/maps.png",
},
{
name: "HubSpot",
text: "Manage customer relationships, automate marketing, and track sales.",
icon: "/integrations/hubspot.png",
},
{
name: "Linear",
text: "Streamline project management and issue tracking with a modern workflow.",
icon: "/integrations/linear.png",
},
{
name: "Medium",
text: "Publish and explore insightful content with a powerful writing platform.",
icon: "/integrations/medium.png",
},
{
name: "Mem0",
text: "AI-powered memory assistant for smarter data organization and recall.",
icon: "/integrations/mem0.png",
},
{
name: "Notion",
text: "Organize work, notes, and databases in an all-in-one workspace.",
icon: "/integrations/notion.png",
},
{
name: "NVIDIA",
text: "Accelerate AI, graphics, and computing with cutting-edge technology.",
icon: "/integrations/nvidia.jpg",
},
{
name: "OpenWeatherMap",
text: "Access real-time weather data and forecasts worldwide.",
icon: "/integrations/openweathermap.png",
},
{
name: "Pinecone",
text: "Store and search vector data for AI-driven applications.",
icon: "/integrations/pinecone.png",
},
{
name: "Reddit",
text: "Explore trending discussions and engage with online communities.",
icon: "/integrations/reddit.png",
},
{
name: "Slant3D",
text: "Automate and optimize 3D printing workflows with AI.",
icon: "/integrations/slant3d.jpeg",
},
{
name: "SMTP",
text: "Send and manage emails with secure and reliable delivery.",
icon: "/integrations/smtp.png",
},
{
name: "Todoist",
text: "Organize tasks and projects with a simple, intuitive to-do list.",
icon: "/integrations/todoist.png",
},
{
name: "Twitter (X)",
text: "Stay connected and share updates on the world's biggest conversation platform.",
icon: "/integrations/x.png",
},
{
name: "Unreal Speech",
text: "Generate natural-sounding AI voices for speech applications.",
icon: "/integrations/unreal-speech.png",
},
];
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
return !str || str.trim().length === 0;
}
export default function Page() {
const { state, setState } = useOnboarding(3);
const switchIntegration = useCallback(
(name: string) => {
const integrations = state.integrations.includes(name)
? state.integrations.filter((i) => i !== name)
: [...state.integrations, name];
setState({ integrations });
},
[state.integrations, setState],
);
return (
<OnboardingStep>
<OnboardingHeader backHref={"/onboarding/2-reason"}>
<OnboardingText className="mt-4" variant="header" center>
What platforms or services would you like AutoGPT to work with?
</OnboardingText>
<OnboardingText className="mt-1" center>
You can select more than one option
</OnboardingText>
</OnboardingHeader>
<div className="w-fit">
<OnboardingText className="my-4" variant="subheader">
Available integrations
</OnboardingText>
<OnboardingGrid
elements={services}
selected={state.integrations}
onSelect={switchIntegration}
/>
<OnboardingText className="mt-12" variant="subheader">
Help us grow our integrations
</OnboardingText>
<OnboardingText className="my-4">
Let us know which partnerships you&apos;d like to see next
</OnboardingText>
<OnboardingInput
className="mb-4"
placeholder="Others (please specify)"
value={state.otherIntegrations || ""}
onChange={(otherIntegrations) => setState({ otherIntegrations })}
/>
</div>
<OnboardingFooter>
<OnboardingButton
className="mb-2"
href="/onboarding/4-agent"
disabled={
state.integrations.length === 0 &&
isEmptyOrWhitespace(state.otherIntegrations)
}
>
Next
</OnboardingButton>
</OnboardingFooter>
</OnboardingStep>
);
}

View File

@@ -0,0 +1,76 @@
"use client";
import OnboardingButton from "@/components/onboarding/OnboardingButton";
import { useOnboarding } from "../layout";
import {
OnboardingFooter,
OnboardingHeader,
OnboardingStep,
} from "@/components/onboarding/OnboardingStep";
import { OnboardingText } from "@/components/onboarding/OnboardingText";
import OnboardingAgentCard from "@/components/onboarding/OnboardingAgentCard";
const agents = [
{
id: "0",
image: "/placeholder.png",
name: "Viral News Video Creator: AI TikTok Shorts",
description:
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
author: "Pwuts",
runs: 1539,
rating: 4.1,
},
{
id: "1",
image: "/placeholder.png",
name: "Financial Analysis Agent: Your Personalized Financial Insights Tool",
description:
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
author: "John Ababseh",
runs: 824,
rating: 4.5,
},
];
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
return !str || str.trim().length === 0;
}
export default function Page() {
const { state, setState } = useOnboarding(4);
return (
<OnboardingStep>
<OnboardingHeader backHref={"/onboarding/3-services"}>
<OnboardingText className="mt-4" variant="header" center>
Choose an agent
</OnboardingText>
<OnboardingText className="mt-1" center>
We think these agents are a good match for you based on your answers
</OnboardingText>
</OnboardingHeader>
<div className="my-12 flex items-center justify-between gap-5">
<OnboardingAgentCard
{...agents[0]}
selected={state.chosenAgentId == "0"}
onClick={() => setState({ chosenAgentId: "0" })}
/>
<OnboardingAgentCard
{...agents[1]}
selected={state.chosenAgentId == "1"}
onClick={() => setState({ chosenAgentId: "1" })}
/>
</div>
<OnboardingFooter>
<OnboardingButton
href="/onboarding/5-run"
disabled={isEmptyOrWhitespace(state.chosenAgentId)}
>
Next
</OnboardingButton>
</OnboardingFooter>
</OnboardingStep>
);
}

View File

@@ -0,0 +1,204 @@
"use client";
import OnboardingButton from "@/components/onboarding/OnboardingButton";
import {
OnboardingStep,
OnboardingHeader,
} from "@/components/onboarding/OnboardingStep";
import { OnboardingText } from "@/components/onboarding/OnboardingText";
import { useOnboarding } from "../layout";
import StarRating from "@/components/onboarding/StarRating";
import { Play } from "lucide-react";
import { cn } from "@/lib/utils";
import { useCallback, useState } from "react";
import OnboardingAgentInput from "@/components/onboarding/OnboardingAgentInput";
import Image from "next/image";
const agents = [
{
id: "0",
image: "/placeholder.png",
name: "Viral News Video Creator: AI TikTok Shorts",
description:
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
author: "Pwuts",
runs: 1539,
rating: 4.1,
},
{
id: "1",
image: "/placeholder.png",
name: "Financial Analysis Agent: Your Personalized Financial Insights Tool",
description:
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
author: "John Ababseh",
runs: 824,
rating: 4.5,
},
];
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
return !str || str.trim().length === 0;
}
export default function Page() {
const { state, setState } = useOnboarding(5);
const [showInput, setShowInput] = useState(false);
const selectedAgent = agents.find(
(agent) => agent.id === state.chosenAgentId,
);
const setAgentInput = useCallback(
(key: string, value: string) => {
setState({
...state,
agentInput: {
...state.agentInput,
[key]: value,
},
});
},
[state, setState],
);
const runYourAgent = (
<div className="ml-[54px] w-[481px] pl-5">
<div className="flex flex-col">
<OnboardingText variant="header">Run your first agent</OnboardingText>
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
A &apos;run&apos; is when your agent starts working on a task
</span>
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
Click on <b>New Run</b> below to try it out
</span>
<div
onClick={() => {
setShowInput(true);
setState({ step: 6 });
}}
className={cn(
"mt-16 flex h-[68px] w-[330px] items-center justify-center rounded-xl border-2 border-violet-700 bg-neutral-50",
"cursor-pointer transition-all duration-200 ease-in-out hover:bg-violet-50",
)}
>
<svg
width="38"
height="38"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<g stroke="#6d28d9" strokeWidth="1.2" strokeLinecap="round">
<line x1="16" y1="8" x2="16" y2="24" />
<line x1="8" y1="16" x2="24" y2="16" />
</g>
</svg>
<span className="ml-3 font-sans text-[19px] font-medium leading-normal text-violet-700">
New run
</span>
</div>
</div>
</div>
);
return (
<OnboardingStep dotted>
<OnboardingHeader backHref={"/onboarding/4-agent"} transparent />
<div
className={cn(
"flex w-full items-center justify-center",
showInput ? "mt-[32px]" : "mt-[192px]",
)}
>
{/* Left side */}
<div className="mr-[52px] w-[481px]">
<div className="h-[156px] w-[481px] rounded-xl bg-white px-6 pb-5 pt-4">
<span className="font-sans text-xs font-medium tracking-wide text-zinc-500">
SELECTED AGENT
</span>
<div className="mt-4 flex h-20 rounded-lg bg-violet-50 p-2">
{/* Left image */}
<Image
src="/placeholder.png"
alt="Description"
width={350}
height={196}
className="h-full w-auto rounded-lg object-contain"
/>
{/* Right content */}
<div className="ml-2 flex flex-1 flex-col">
<span className="w-[292px] truncate font-sans text-[14px] font-medium leading-normal text-zinc-800">
{selectedAgent?.name}
</span>
<span className="mt-[5px] w-[292px] truncate font-sans text-xs font-normal leading-tight text-zinc-600">
by {selectedAgent?.author}
</span>
<div className="mt-auto flex w-[292px] justify-between">
<span className="mt-1 truncate font-sans text-xs font-normal leading-tight text-zinc-600">
{selectedAgent?.runs.toLocaleString("en-US")} runs
</span>
<StarRating
className="font-sans text-xs font-normal leading-tight text-zinc-600"
starSize={12}
rating={selectedAgent?.rating || 0}
/>
</div>
</div>
</div>
</div>
</div>
{/* Right side */}
{!showInput ? (
runYourAgent
) : (
<div className="ml-[54px] w-[481px] pl-5">
<div className="flex flex-col">
<OnboardingText variant="header">
Provide details for your agent
</OnboardingText>
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
Give your agent the details it needs to workjust enter <br />
the key information and get started.
</span>
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
When you&apos;re done, click <b>Run Agent</b>.
</span>
<div className="mt-12 inline-flex w-[492px] flex-col items-start justify-start gap-2 rounded-[20px] border border-zinc-300 bg-white p-6">
<OnboardingText className="mb-3 font-semibold" variant="header">
Input
</OnboardingText>
<OnboardingAgentInput
name={"Video Count"}
description={"The number of videos you'd like to generate"}
placeholder={"eg. 1"}
value={state.agentInput?.videoCount || ""}
onChange={(v) => setAgentInput("videoCount", v)}
/>
<OnboardingAgentInput
name={"Source Website"}
description={"The website to source the stories from"}
placeholder={"eg. youtube URL"}
value={state.agentInput?.sourceWebsite || ""}
onChange={(v) => setAgentInput("sourceWebsite", v)}
/>
</div>
<OnboardingButton
variant="violet"
className="mt-8 w-[136px]"
disabled={
isEmptyOrWhitespace(state.agentInput?.videoCount) ||
isEmptyOrWhitespace(state.agentInput?.sourceWebsite)
}
>
<Play className="" size={18} />
Run agent
</OnboardingButton>
</div>
</div>
)}
</div>
</OnboardingStep>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
type OnboardingState = {
step: number;
usageReason?: string;
integrations: string[];
otherIntegrations?: string;
chosenAgentId?: string;
agentInput?: { [key: string]: string };
};
const OnboardingContext = createContext<
| {
state: OnboardingState;
setState: (state: Partial<OnboardingState>) => void;
}
| undefined
>(undefined);
export function useOnboarding(step?: number) {
const context = useContext(OnboardingContext);
if (!context)
throw new Error("useOnboarding must be used within OnboardingLayout");
useEffect(() => {
if (!step) return;
context.setState({ step });
}, [step]);
return context;
}
export default function OnboardingLayout({
children,
}: {
children: ReactNode;
}) {
const [state, setStateRaw] = useState<OnboardingState>({
step: 0,
integrations: [],
});
const setState = (newState: Partial<OnboardingState>) => {
setStateRaw((prev) => ({ ...prev, ...newState }));
};
return (
<OnboardingContext.Provider value={{ state, setState }}>
<div className="flex min-h-screen w-full items-center justify-center bg-gray-100">
<div className="mx-auto flex w-full flex-col items-center">
{children}
</div>
</div>
</OnboardingContext.Provider>
);
}

View File

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function OnboardingPage() {
redirect("/onboarding/1-welcome");
}

View File

@@ -0,0 +1,99 @@
import { cn } from "@/lib/utils";
import Image from "next/image";
import StarRating from "./StarRating";
interface OnboardingAgentCardProps {
id: string;
image: string;
name: string;
description: string;
author: string;
runs: number;
rating: number;
selected?: boolean;
onClick: () => void;
}
export default function OnboardingAgentCard({
id,
image,
name,
description,
author,
runs,
rating,
selected,
onClick,
}: OnboardingAgentCardProps) {
return (
<div
className={cn(
"relative cursor-pointer transition-all duration-200 ease-in-out",
"h-[394px] w-[368px] rounded-xl border border-transparent bg-white",
selected ? "bg-[#F5F3FF80]" : "hover:border-zinc-400",
)}
onClick={onClick}
>
{/* Image container */}
<div className="relative">
<Image
src={image}
alt="Agent cover"
className="m-2 h-[196px] w-[350px] rounded-xl object-cover"
width={350}
height={196}
/>
{/* Profile picture overlay */}
<div className="absolute bottom-2 left-4">
<Image
src={image}
alt="Profile picture"
className="h-[50px] w-[50px] rounded-full border border-white object-cover object-center"
width={50}
height={50}
/>
</div>
</div>
{/* Content container */}
<div className="flex h-[180px] flex-col justify-between px-4 pb-3">
{/* Text content wrapper */}
<div>
{/* Title - 2 lines max */}
<p className="text-md line-clamp-2 max-h-[50px] font-sans text-base font-medium leading-normal text-zinc-800">
{name}
</p>
{/* Author - single line with truncate */}
<p className="truncate text-sm font-normal leading-normal text-zinc-600">
by {author}
</p>
{/* Description - 3 lines max */}
<p
className={cn(
"mt-2 line-clamp-3 text-sm leading-5",
selected ? "text-zinc-500" : "text-zinc-400",
)}
>
{description}
</p>
</div>
{/* Bottom stats */}
<div className="flex w-full items-center justify-between">
<span className="mt-1 font-sans text-sm font-medium text-zinc-800">
{runs.toLocaleString("en-US")} runs
</span>
<StarRating rating={rating} />
</div>
</div>
<div
className={cn(
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
selected ? "border-violet-700" : "border-transparent",
)}
/>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { cn } from "@/lib/utils";
interface OnboardingAgentInputProps {
className?: string;
name: string;
description: string;
placeholder: string;
value: string;
onChange: (value: string) => void;
}
export default function OnboardingAgentInput({
className,
name,
description,
placeholder,
value,
onChange,
}: OnboardingAgentInputProps) {
return (
<>
<span className="text=black font-sans text-sm font-medium leading-tight">
{name}
</span>
<span className="text-sm font-normal leading-tight text-slate-500">
{description}
</span>
<input
className={cn(
className,
"relative inline-flex h-11 w-[444px] items-center justify-start rounded-[55px] border border-slate-200 px-4 py-2.5 font-sans text-sm placeholder:text-zinc-400",
"truncate transition-all duration-200 ease-in-out",
"focus:border-transparent focus:bg-[#F5F3FF80] focus:outline-none focus:ring-2 focus:ring-violet-700",
)}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</>
);
}

View File

@@ -0,0 +1,20 @@
import { ChevronLeft } from "lucide-react";
import Link from "next/link";
interface OnboardingBackButtonProps {
href: string;
}
export default function OnboardingBackButton({
href,
}: OnboardingBackButtonProps) {
return (
<Link
className="flex items-center gap-2 font-sans text-base font-medium text-zinc-700 transition-colors duration-200 hover:text-zinc-800"
href={href}
>
<ChevronLeft size={24} className="-mr-1" />
<span>Back</span>
</Link>
);
}

View File

@@ -0,0 +1,48 @@
import { cn } from "@/lib/utils";
import Link from "next/link";
const variants = {
default: "bg-zinc-700 hover:bg-zinc-800",
violet: "bg-violet-600 hover:bg-violet-700",
};
type OnboardingButtonProps = {
className?: string;
variant?: keyof typeof variants;
children?: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
href?: string;
};
export default function OnboardingButton({
className,
variant = "default",
children,
disabled,
onClick,
href,
}: OnboardingButtonProps) {
const buttonClasses = cn(
"font-sans text-white text-sm font-medium",
"inline-flex justify-center items-center",
"h-12 min-w-[100px] rounded-full py-3 px-5 gap-2.5",
"transition-colors duration-200",
className,
disabled ? "bg-zinc-300 cursor-not-allowed" : variants[variant],
);
if (href && !disabled) {
return (
<Link href={href} className={buttonClasses}>
{children}
</Link>
);
}
return (
<button onClick={onClick} disabled={disabled} className={buttonClasses}>
{children}
</button>
);
}

View File

@@ -0,0 +1,87 @@
import { cn } from "@/lib/utils";
import Image from "next/image";
type OnboardingGridElementProps = {
name: string;
text: string;
icon: string;
selected: boolean;
onClick: () => void;
};
function OnboardingGridElement({
name,
text,
icon,
selected,
onClick,
}: OnboardingGridElementProps) {
return (
<button
className={cn(
"relative flex h-[236px] w-[200px] flex-col items-start gap-2 rounded-xl border border-transparent bg-white p-[15px] font-sans",
"transition-all duration-200 ease-in-out",
selected ? "bg-[#F5F3FF80]" : "hover:border-zinc-400",
)}
onClick={onClick}
>
<Image
src={icon}
alt={`Logo of ${name}`}
className="h-12 w-12 rounded-lg object-contain object-center"
width={48}
height={48}
/>
<span className="text-md mt-4 w-full text-left font-medium leading-normal text-[#121212]">
{name}
</span>
<span className="w-full text-left text-[11.5px] font-normal leading-5 text-zinc-500">
{text}
</span>
<div
className={cn(
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
selected ? "border-violet-700" : "border-transparent",
)}
/>
</button>
);
}
type OnboardingGridProps = {
className?: string;
elements: Array<{
name: string;
text: string;
icon: string;
}>;
selected?: string[];
onSelect: (name: string) => void;
};
export function OnboardingGrid({
className,
elements,
selected,
onSelect,
}: OnboardingGridProps) {
return (
<div
className={cn(
className,
"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4",
)}
>
{elements.map((element) => (
<OnboardingGridElement
key={element.name}
name={element.name}
text={element.text}
icon={element.icon}
selected={selected?.includes(element.name) || false}
onClick={() => onSelect(element.name)}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,30 @@
import { cn } from "@/lib/utils";
interface OnboardingInputProps {
className?: string;
placeholder: string;
value: string;
onChange: (value: string) => void;
}
export default function OnboardingInput({
className,
placeholder,
value,
onChange,
}: OnboardingInputProps) {
return (
<input
className={cn(
className,
"relative h-[50px] w-[512px] rounded-[25px] border border-transparent bg-white px-4",
"font-poppin text-sm font-normal leading-normal text-zinc-900 placeholder:text-zinc-400",
"transition-all duration-200 ease-in-out",
"focus:border-transparent focus:bg-[#F5F3FF80] focus:outline-none focus:ring-2 focus:ring-violet-700",
)}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}

View File

@@ -0,0 +1,136 @@
import { cn } from "@/lib/utils";
import { Check } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
type OnboardingListElementProps = {
label: string;
text: string;
selected?: boolean;
custom?: boolean;
onClick: (content: string) => void;
};
type OnboardingListProps = {
className?: string;
elements: Array<{
label: string;
text: string;
id: string;
}>;
selectedId?: string;
onSelect: (id: string) => void;
};
function OnboardingListElement({
label,
text,
selected,
custom,
onClick,
}: OnboardingListElementProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [content, setContent] = useState(text);
useEffect(() => {
if (selected && custom && inputRef.current) {
inputRef.current.focus();
}
}, [selected, custom]);
const setCustomText = (e: React.ChangeEvent<HTMLInputElement>) => {
setContent(e.target.value);
onClick(e.target.value);
};
return (
<button
onClick={() => onClick(content)}
className={cn(
"relative flex h-[78px] w-[530px] items-center rounded-xl px-5 py-4",
"border border-transparent",
"transition-all duration-200 ease-in-out",
selected ? "bg-[#F5F3FF80]" : "bg-white hover:border-zinc-400",
)}
>
<div className="flex flex-col items-start gap-1">
<span className="text-sm font-medium text-zinc-700">{label}</span>
{custom && selected ? (
<input
ref={inputRef}
className={cn(
selected ? "text-zinc-600" : "text-zinc-400",
"font-poppin border-0 bg-[#F5F3FF80] text-sm focus:outline-none",
)}
placeholder="Please specify"
value={content}
onChange={setCustomText}
/>
) : (
<span
className={cn(
selected ? "text-zinc-600" : "text-zinc-400",
"text-sm",
)}
>
{custom ? "Please specify" : text}
</span>
)}
</div>
{!custom && (
<div className="absolute right-4">
<Check
size={24}
className={cn(
"transition-all duration-200 ease-in-out",
selected ? "text-violet-700" : "text-transparent",
)}
/>
</div>
)}
<div
className={cn(
"pointer-events-none absolute inset-0 rounded-xl border-2",
"transition-all duration-200 ease-in-out",
selected ? "border-violet-700" : "border-transparent",
)}
/>
</button>
);
}
export default function OnboardingList({
className,
elements,
selectedId,
onSelect,
}: OnboardingListProps) {
const isCustom = useCallback(() => {
return (
selectedId !== undefined &&
!elements.some((element) => element.id === selectedId)
);
}, [selectedId, elements]);
return (
<div className={cn(className, "flex flex-col gap-2")}>
{elements.map((element) => (
<OnboardingListElement
key={element.id}
label={element.label}
text={element.text}
selected={element.id === selectedId}
onClick={() => onSelect(element.id)}
/>
))}
<OnboardingListElement
label="Other"
text={isCustom() ? selectedId! : ""}
selected={isCustom()}
custom
onClick={(c) => {
onSelect(c);
}}
/>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import { useState, useEffect, useRef } from "react";
interface OnboardingProgressProps {
totalSteps: number;
toStep: number;
}
export default function OnboardingProgress({
totalSteps,
toStep,
}: OnboardingProgressProps) {
const [animatedStep, setAnimatedStep] = useState(toStep - 1);
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
// On initial mount, just set the position without animation
isInitialMount.current = false;
return;
}
// After initial mount, animate position changes
setAnimatedStep(toStep - 1);
}, [toStep]);
return (
<div className="relative flex items-center justify-center gap-3">
{/* Background circles */}
{Array.from({ length: totalSteps + 1 }).map((_, index) => (
<div key={index} className="h-2 w-2 rounded-full bg-zinc-400" />
))}
{/* Animated progress indicator */}
<div
className={`absolute left-0 h-2 w-7 rounded-full bg-zinc-400 ${
!isInitialMount.current
? "transition-all duration-300 ease-in-out"
: ""
}`}
style={{
transform: `translateX(${animatedStep * 20}px)`,
}}
/>
</div>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
import { ReactNode } from "react";
import OnboardingBackButton from "./OnboardingBackButton";
import { cn } from "@/lib/utils";
import OnboardingProgress from "./OnboardingProgress";
import { useOnboarding } from "@/app/onboarding/layout";
export function OnboardingStep({
dotted,
children,
}: {
dotted?: boolean;
children: ReactNode;
}) {
return (
<div className="relative flex min-h-screen w-full flex-col">
{dotted && (
<div className="absolute left-1/2 h-full w-1/2 bg-white bg-[radial-gradient(#e5e7eb77_1px,transparent_1px)] [background-size:10px_10px]"></div>
)}
<div className="z-10 flex flex-col items-center">{children}</div>
</div>
);
}
interface OnboardingHeaderProps {
backHref: string;
transparent?: boolean;
children?: ReactNode;
}
export function OnboardingHeader({
backHref,
transparent,
children,
}: OnboardingHeaderProps) {
const { state } = useOnboarding();
return (
<div className="sticky top-0 z-10 w-full">
<div
className={cn(transparent ? "bg-transparent" : "bg-gray-100", "pb-5")}
>
<div className="flex w-full items-center justify-between px-5 py-4">
<OnboardingBackButton href={backHref} />
<OnboardingProgress totalSteps={5} toStep={state.step - 1} />
</div>
{children}
</div>
{!transparent && (
<div className="h-4 w-full bg-gradient-to-b from-gray-100 via-gray-100/50 to-transparent" />
)}
</div>
);
}
export function OnboardingFooter({ children }: { children?: ReactNode }) {
return (
<div className="sticky bottom-0 z-10 w-full">
<div className="h-4 w-full bg-gradient-to-t from-gray-100 via-gray-100/50 to-transparent" />
<div className="flex justify-center bg-gray-100">
<div className="px-5 py-5">{children}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { cn } from "@/lib/utils";
import { ReactNode } from "react";
const variants = {
header: "font-poppin text-xl font-medium leading-7 text-zinc-900",
subheader: "font-sans text-sm font-medium leading-6 text-zinc-800",
default: "font-sans text-sm font-normal leading-6 text-zinc-500",
};
export function OnboardingText({
className,
center,
variant = "default",
children,
}: {
className?: string;
center?: boolean;
variant?: keyof typeof variants;
children: ReactNode;
}) {
return (
<div
className={cn(
"w-full",
center ? "text-center" : "text-left",
variants[variant] || variants.default,
className,
)}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { cn } from "@/lib/utils";
import { FaRegStar, FaStar, FaStarHalfAlt } from "react-icons/fa";
export default function StarRating({
className,
starSize,
rating,
}: {
className?: string;
starSize?: number;
rating: number;
}) {
// Round to 1 decimal place
const roundedRating = Math.round(rating * 10) / 10;
starSize ??= 15;
// Generate array of 5 star values
const stars = Array(5)
.fill(0)
.map((_, index) => {
const difference = roundedRating - index;
if (difference >= 1) {
return "full";
} else if (difference > 0) {
// Half star for values between 0.2 and 0.8
return difference >= 0.8
? "full"
: difference >= 0.2
? "half"
: "empty";
}
return "empty";
});
return (
<div
className={cn(
"font-geist flex items-center gap-0.5 text-sm font-medium text-zinc-800",
className,
)}
>
{/* Display numerical rating */}
<span className="mr-1 mt-1">{roundedRating}</span>
{/* Display stars */}
{stars.map((starType, index) => {
if (starType === "full") {
return <FaStar size={starSize} key={index} />;
} else if (starType === "half") {
return <FaStarHalfAlt size={starSize} key={index} />;
} else {
return <FaRegStar size={starSize} key={index} />;
}
})}
</div>
);
}

View File

@@ -8,6 +8,7 @@ const PROTECTED_PAGES = [
"/marketplace/profile",
"/marketplace/settings",
"/marketplace/dashboard",
"/onboarding",
];
const ADMIN_PAGES = ["/admin"];

View File

@@ -2,6 +2,7 @@ import { updateSession } from "@/lib/supabase/middleware";
import { type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
request.headers.set("x-current-path", request.nextUrl.pathname);
return await updateSession(request);
}