mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
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:
committed by
GitHub
parent
9471fd6b58
commit
e22d2c848a
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Home() {
|
||||
|
||||
useEffect(() => {
|
||||
completeStep("BUILDER_OPEN");
|
||||
}, []);
|
||||
}, [completeStep]);
|
||||
|
||||
return (
|
||||
<FlowEditor
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
if (isUserLoading || user) {
|
||||
return <Spinner />;
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function PrivatePage() {
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
return <Spinner />;
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!user || !supabase) {
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
return <Spinner />;
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function SignupPage() {
|
||||
}
|
||||
|
||||
if (isUserLoading || user) {
|
||||
return <Spinner />;
|
||||
return <Spinner className="h-[80vh]" />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user