feat(frontend): Onboarding Updates 3 (#9916)

A collection of UX update and bug fixes for onboarding and wallet.

### Changes 🏗️

- Show spinner loading indicator when onboarding button is clicked
- Use `getLibraryAgentByStoreListingVersionID` instead of
`addMarketplaceAgentToLibrary` on congrats screen
- Fix `Not enough segments` issue: don't fetch onboarding when user is
logged out
- Minor updates
  - Fill some missing deps in deps arrays
  - `Spinner` component, styles updates
  - Use `useMemo`/`useCallback`
- Show error toast when onboarding agent fails to run:

<img width="405" alt="Screenshot 2025-05-06 at 5 09 01 PM"
src="https://github.com/user-attachments/assets/dd1272da-326a-448d-995d-98ac773b3ee4"
/>

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Onboarding can be completed
  - [x] Failing agent shows toast
  - [x] Wallet can be opened and works properly (tasks, confetti)
  - [x] Dependency arrays don't cause infinite loops
This commit is contained in:
Krzysztof Czerwinski
2025-05-14 16:29:29 +02:00
committed by GitHub
parent 9471fd6b58
commit e22d2c848a
13 changed files with 125 additions and 58 deletions

View File

@@ -17,6 +17,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import SchemaTooltip from "@/components/SchemaTooltip";
import { TypeBasedInput } from "@/components/type-based-input";
import SmartImage from "@/components/agptui/SmartImage";
import { useToast } from "@/components/ui/use-toast";
export default function Page() {
const { state, updateState, setStep } = useOnboarding(
@@ -26,6 +27,8 @@ export default function Page() {
const [showInput, setShowInput] = useState(false);
const [agent, setAgent] = useState<GraphMeta | null>(null);
const [storeAgent, setStoreAgent] = useState<StoreAgentDetails | null>(null);
const [runningAgent, setRunningAgent] = useState(false);
const { toast } = useToast();
const router = useRouter();
const api = useBackendAPI();
@@ -76,27 +79,35 @@ export default function Page() {
[state?.agentInput, updateState],
);
const runAgent = useCallback(() => {
const runAgent = useCallback(async () => {
if (!agent) {
return;
}
api
.addMarketplaceAgentToLibrary(storeAgent?.store_listing_version_id || "")
.then((libraryAgent) => {
api
.executeGraph(
libraryAgent.graph_id,
libraryAgent.graph_version,
state?.agentInput || {},
)
.then(({ graph_exec_id }) => {
updateState({
onboardingAgentExecutionId: graph_exec_id,
});
router.push("/onboarding/6-congrats");
});
setRunningAgent(true);
try {
const libraryAgent = await api.addMarketplaceAgentToLibrary(
storeAgent?.store_listing_version_id || "",
);
const { graph_exec_id } = await api.executeGraph(
libraryAgent.graph_id,
libraryAgent.graph_version,
state?.agentInput || {},
);
updateState({
onboardingAgentExecutionId: graph_exec_id,
});
}, [api, agent, router, state?.agentInput, storeAgent, updateState]);
router.push("/onboarding/6-congrats");
} catch (error) {
console.error("Error running agent:", error);
toast({
title: "Error running agent",
description:
"There was an error running your agent. Please try again or try choosing a different agent if it still fails.",
variant: "destructive",
});
setRunningAgent(false);
}
}, [api, agent, router, state?.agentInput, storeAgent, updateState, toast]);
const runYourAgent = (
<div className="ml-[104px] w-[481px] pl-5">
@@ -234,14 +245,17 @@ export default function Page() {
<OnboardingButton
variant="violet"
className="mt-8 w-[136px]"
loading={runningAgent}
disabled={
Object.values(state?.agentInput || {}).some(
(value) => String(value).trim() === "",
) || !agent
) ||
!agent ||
runningAgent
}
onClick={runAgent}
icon={<Play className="mr-2" size={18} />}
>
<Play className="" size={18} />
Run agent
</OnboardingButton>
</div>

View File

@@ -6,9 +6,10 @@ import { redirect } from "next/navigation";
export async function finishOnboarding() {
const api = new BackendAPI();
const onboarding = await api.getUserOnboarding();
const listingId = onboarding?.selectedStoreListingVersionId;
if (listingId) {
const libraryAgent = await api.addMarketplaceAgentToLibrary(listingId);
const libraryAgent = await api.getLibraryAgentByStoreListingVersionID(
onboarding?.selectedStoreListingVersionId || "",
);
if (libraryAgent) {
revalidatePath(`/library/agents/${libraryAgent.id}`, "layout");
redirect(`/library/agents/${libraryAgent.id}`);
} else {

View File

@@ -6,7 +6,7 @@ import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import * as party from "party-js";
export default function Page() {
const { state, updateState } = useOnboarding(7, "AGENT_INPUT");
const { completeStep } = useOnboarding(7, "AGENT_INPUT");
const [showText, setShowText] = useState(false);
const [showSubtext, setShowSubtext] = useState(false);
const divRef = useRef(null);
@@ -17,7 +17,7 @@ export default function Page() {
count: 100,
spread: 180,
shapes: ["square", "circle"],
size: party.variation.range(2, 2), // scalar: 2
size: party.variation.range(2, 2.5),
speed: party.variation.range(300, 1000),
});
}
@@ -31,9 +31,7 @@ export default function Page() {
}, 500);
const timer2 = setTimeout(() => {
updateState({
completedSteps: [...(state?.completedSteps || []), "CONGRATS"],
});
completeStep("CONGRATS");
finishOnboarding();
}, 3000);

View File

@@ -12,7 +12,7 @@ export default function Home() {
useEffect(() => {
completeStep("BUILDER_OPEN");
}, []);
}, [completeStep]);
return (
<FlowEditor

View File

@@ -98,7 +98,7 @@ export default function LoginPage() {
}
if (isUserLoading || user) {
return <Spinner />;
return <Spinner className="h-[80vh]" />;
}
if (!supabase) {

View File

@@ -122,7 +122,7 @@ export default function PrivatePage() {
);
if (isUserLoading) {
return <Spinner />;
return <Spinner className="h-[80vh]" />;
}
if (!user || !supabase) {

View File

@@ -134,7 +134,7 @@ export default function ResetPasswordPage() {
);
if (isUserLoading) {
return <Spinner />;
return <Spinner className="h-[80vh]" />;
}
if (!supabase) {

View File

@@ -94,7 +94,7 @@ export default function SignupPage() {
}
if (isUserLoading || user) {
return <Spinner />;
return <Spinner className="h-[80vh]" />;
}
if (!supabase) {

View File

@@ -1,9 +1,11 @@
import { FaSpinner } from "react-icons/fa";
import { LoaderCircle } from "lucide-react";
export default function Spinner({ className }: { className?: string }) {
const spinnerClasses = `mr-2 h-16 w-16 animate-spin ${className || ""}`;
export default function Spinner() {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
<div className="flex items-center justify-center">
<LoaderCircle className={spinnerClasses} />
</div>
);
}

View File

@@ -11,7 +11,7 @@ import { PopoverClose } from "@radix-ui/react-popover";
import { TaskGroups } from "../onboarding/WalletTaskGroups";
import { ScrollArea } from "../ui/scroll-area";
import { useOnboarding } from "../onboarding/onboarding-provider";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import * as party from "party-js";
import WalletRefill from "./WalletRefill";
@@ -36,11 +36,15 @@ export default function Wallet() {
fetchCredits();
}, [state?.notificationDot, updateState, fetchCredits]);
const fadeOut = new party.ModuleBuilder()
.drive("opacity")
.by((t) => 1 - t)
.through("lifetime")
.build();
const fadeOut = useMemo(
() =>
new party.ModuleBuilder()
.drive("opacity")
.by((t) => 1 - t)
.through("lifetime")
.build(),
[],
);
// Confetti effect on the wallet button
useEffect(() => {
@@ -74,7 +78,14 @@ export default function Wallet() {
});
}, 800);
}
}, [state?.completedSteps, state?.notified]);
}, [
state?.completedSteps,
state?.notified,
fadeOut,
fetchCredits,
stepsLength,
walletRef,
]);
// Wallet flash on credits change
useEffect(() => {
@@ -89,7 +100,7 @@ export default function Wallet() {
setTimeout(() => {
setFlash(false);
}, 300);
}, [credits]);
}, [credits, prevCredits]);
return (
<Popover>

View File

@@ -1,5 +1,7 @@
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useCallback, useMemo, useState } from "react";
import Spinner from "../Spinner";
const variants = {
default: "bg-zinc-700 hover:bg-zinc-800",
@@ -10,38 +12,64 @@ type OnboardingButtonProps = {
className?: string;
variant?: keyof typeof variants;
children?: React.ReactNode;
loading?: boolean;
disabled?: boolean;
onClick?: () => void;
href?: string;
icon?: React.ReactNode;
};
export default function OnboardingButton({
className,
variant = "default",
children,
loading,
disabled,
onClick,
href,
icon,
}: 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],
const [internalLoading, setInternalLoading] = useState(false);
const isLoading = loading !== undefined ? loading : internalLoading;
const buttonClasses = useMemo(
() =>
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",
"transition-colors duration-200",
className,
disabled ? "bg-zinc-300 cursor-not-allowed" : variants[variant],
),
[disabled, variant, className],
);
const onClickInternal = useCallback(() => {
setInternalLoading(true);
if (onClick) {
onClick();
}
}, [setInternalLoading, onClick]);
if (href && !disabled) {
return (
<Link href={href} className={buttonClasses}>
<Link href={href} onClick={onClickInternal} className={buttonClasses}>
{isLoading && <Spinner className="h-5 w-5" />}
{icon && !isLoading && <>{icon}</>}
{children}
</Link>
);
}
return (
<button onClick={onClick} disabled={disabled} className={buttonClasses}>
<button
onClick={onClickInternal}
disabled={disabled}
className={buttonClasses}
>
{isLoading && <Spinner className="h-5 w-5" />}
{icon && !isLoading && <>{icon}</>}
{children}
</button>
);

View File

@@ -187,7 +187,15 @@ export function TaskGroups() {
}
});
});
}, [state?.completedSteps, delayConfetti]);
}, [
state?.completedSteps,
delayConfetti,
groups,
updateState,
state?.notified,
isGroupCompleted,
isTaskCompleted,
]);
return (
<div className="space-y-2">

View File

@@ -1,4 +1,5 @@
"use client";
import useSupabase from "@/hooks/useSupabase";
import { OnboardingStep, UserOnboarding } from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { usePathname, useRouter } from "next/navigation";
@@ -40,13 +41,13 @@ export function useOnboarding(step?: number, completeStep?: OnboardingStep) {
context.updateState({
completedSteps: [...context.state.completedSteps, completeStep],
});
}, [completeStep, context.state, context.updateState]);
}, [completeStep, context, context.updateState]);
useEffect(() => {
if (step && context.step !== step) {
context.setStep(step);
}
}, [step, context.step, context.setStep]);
}, [step, context]);
return context;
}
@@ -62,6 +63,7 @@ export default function OnboardingProvider({
const api = useBackendAPI();
const pathname = usePathname();
const router = useRouter();
const { user, isUserLoading } = useSupabase();
useEffect(() => {
const fetchOnboarding = async () => {
@@ -83,8 +85,11 @@ export default function OnboardingProvider({
router.push("/marketplace");
}
};
if (isUserLoading || !user) {
return;
}
fetchOnboarding();
}, [api, pathname, router]);
}, [api, pathname, router, user, isUserLoading]);
const updateState = useCallback(
(newState: Omit<Partial<UserOnboarding>, "rewardedFor">) => {
@@ -121,7 +126,7 @@ export default function OnboardingProvider({
completedSteps: [...state.completedSteps, step],
});
},
[api, state],
[state, updateState],
);
return (