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: --> - [ ] ...
67
autogpt_platform/frontend/public/gpt_dark_RGB.svg
Normal 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 |
BIN
autogpt_platform/frontend/public/integrations/d-id.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
autogpt_platform/frontend/public/integrations/discord.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
autogpt_platform/frontend/public/integrations/github.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
autogpt_platform/frontend/public/integrations/google.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
autogpt_platform/frontend/public/integrations/hubspot.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
autogpt_platform/frontend/public/integrations/linear.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
autogpt_platform/frontend/public/integrations/maps.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
autogpt_platform/frontend/public/integrations/medium.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
autogpt_platform/frontend/public/integrations/mem0.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
autogpt_platform/frontend/public/integrations/notion.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
autogpt_platform/frontend/public/integrations/nvidia.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
autogpt_platform/frontend/public/integrations/openweathermap.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
autogpt_platform/frontend/public/integrations/pinecone.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
autogpt_platform/frontend/public/integrations/reddit.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
autogpt_platform/frontend/public/integrations/slant3d.jpeg
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
autogpt_platform/frontend/public/integrations/smtp.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
autogpt_platform/frontend/public/integrations/todoist.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
autogpt_platform/frontend/public/integrations/unreal-speech.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
autogpt_platform/frontend/public/integrations/x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
autogpt_platform/frontend/public/placeholder.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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>
|
||||
|
||||
@@ -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's learn a bit about
|
||||
you to
|
||||
<br />
|
||||
tailor your experience.
|
||||
</OnboardingText>
|
||||
<OnboardingButton href="/onboarding/2-reason">Continue</OnboardingButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
178
autogpt_platform/frontend/src/app/onboarding/3-services/page.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
204
autogpt_platform/frontend/src/app/onboarding/5-run/page.tsx
Normal 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 'run' 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 work—just enter <br />
|
||||
the key information and get started.
|
||||
</span>
|
||||
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
|
||||
When you'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>
|
||||
);
|
||||
}
|
||||
64
autogpt_platform/frontend/src/app/onboarding/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
autogpt_platform/frontend/src/app/onboarding/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
redirect("/onboarding/1-welcome");
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const PROTECTED_PAGES = [
|
||||
"/marketplace/profile",
|
||||
"/marketplace/settings",
|
||||
"/marketplace/dashboard",
|
||||
"/onboarding",
|
||||
];
|
||||
const ADMIN_PAGES = ["/admin"];
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||