diff --git a/app/src/components/onboarding/intro-animation.tsx b/app/src/components/onboarding/intro-animation.tsx new file mode 100644 index 0000000000..2ed60fa3b1 --- /dev/null +++ b/app/src/components/onboarding/intro-animation.tsx @@ -0,0 +1,51 @@ +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { type ReactNode, useEffect, useState } from "react"; + +export type IntroScene = { id: string; label: string; content: ReactNode }; + +/** A looping introduction instead of more wizard steps. */ +export function IntroAnimation({ scenes }: { scenes: IntroScene[] }) { + const reducedMotion = useReducedMotion(); + const [scene, setScene] = useState(0); + useEffect(() => { + if (reducedMotion || scenes.length < 2) return; + const timer = window.setInterval( + () => setScene((current) => (current + 1) % scenes.length), + 4000, + ); + return () => window.clearInterval(timer); + }, [reducedMotion, scenes.length]); + + // Keep every scene available without auto-updating content when motion is reduced. + if (reducedMotion) { + return ( +
+ {scenes.map((item) => ( +
{item.content}
+ ))} +
+ ); + } + + return ( +
+
+ + + {scenes[scene]?.content} + + +
+
+ ); +} diff --git a/app/src/routes/_authed/onboarding.tsx b/app/src/routes/_authed/onboarding.tsx index 4319ebee72..0cf102c45f 100644 --- a/app/src/routes/_authed/onboarding.tsx +++ b/app/src/routes/_authed/onboarding.tsx @@ -12,6 +12,7 @@ import { DesktopIllustration } from "@/components/computer/desktop-illustration" import { ComputerPlaceholder } from "@/components/computer/placeholder"; import { SELF_HOST_URL } from "@/components/layout/self-host-banner"; import { IllustrationFrame } from "@/components/onboarding/illustration-frame"; +import { IntroAnimation } from "@/components/onboarding/intro-animation"; import { LaptopIllustration } from "@/components/onboarding/laptop-illustration"; import { OrganizationFlow } from "@/components/onboarding/organization-flow"; import { OnboardingNameStep } from "@/components/settings/profile"; @@ -169,7 +170,7 @@ function RosterStep() { } /* - * THE THREE SLIDES BELOW ARE FOR SOMEBODY TRYING OPENBOT ON THEIR OWN MACHINE, and only they see them + * These local-evaluation scenes are for somebody trying OpenBot on their own machine * (see `localEvaluation` in RouteComponent). They answer what that person is deciding: where their * data is, whether their own agents fit, and what taking it to their organization involves (OB-02 in * the product backlog). None of it is said to an organization's own people, who are past all three. @@ -265,11 +266,39 @@ function OrganizationStep() { ); } -const STEPS: Array<(onNext: () => void) => React.ReactNode> = [ - () => , - () => , - () => , -]; +function Introduction({ localEvaluation }: { localEvaluation: boolean }) { + return ( + }, + { + id: "computer", + label: "Each Bot has its own computer", + content: , + }, + { + id: "roster", + label: "Pick a Bot or make your own", + content: , + }, + ...(localEvaluation + ? [ + { + id: "local", + label: "It runs on your machine", + content: , + }, + { + id: "agents", + label: "Bring the agents you already have", + content: , + }, + ] + : []), + ]} + /> + ); +} /** A pane arrives from the side the journey is moving toward, and leaves out the other. */ const variants = { @@ -283,14 +312,16 @@ function RouteComponent() { const complete = useMutation(completeOnboardingMutationOptions(queryClient)); // Browser state on purpose: the step is not persisted while the wizard is being designed. - const [step, setStep] = React.useState(0); + const [step, setStep] = React.useState<"intro" | "organization" | "name">( + "intro", + ); const [direction, setDirection] = React.useState(1); // The way out: set once the completion is saved, it fades the whole page and then navigates. const [leaving, setLeaving] = React.useState(false); const [ref, bounds] = useMeasure(); /* - * The three local-evaluation slides are never shown to an organization's people: + * Local-evaluation content is never shown to an organization's people: * the server says single-user mode, which an organization's deployment, always behind sign-in, * cannot be in. Until the answer arrives they are left out, so a slow or failed request never * shows an organization's employees a page about deploying OpenBot themselves. @@ -299,18 +330,17 @@ function RouteComponent() { * Intelligence entitlement, and the stack `copilotkit local` installs runs on an enterprise * evaluation licence, so every CLI evaluator, the person these slides are for, would never see them. */ - const { data: capabilities } = useQuery(deploymentCapabilitiesQueryOptions()); - const steps: Array<(onFinish: () => Promise) => React.ReactNode> = [ - ...STEPS, - ...(capabilities?.localEvaluation - ? [() => , () => , () => ] - : []), - (onFinish) => , - ]; - const last = step === steps.length - 1; + const { + data: capabilities, + error: capabilitiesError, + refetch: retryCapabilities, + isFetching: loadingCapabilities, + } = useQuery(deploymentCapabilitiesQueryOptions()); + const localEvaluation = capabilities?.localEvaluation === true; + const last = step === "name"; - const go = (to: number) => { - setDirection(to > step ? 1 : -1); + const go = (to: typeof step, forward: boolean) => { + setDirection(forward ? 1 : -1); setStep(to); }; @@ -351,13 +381,36 @@ function RouteComponent() { key={step} variants={variants} > - {steps[step]?.(async () => { - await complete.mutateAsync(undefined); - setLeaving(true); - })} + {step === "intro" && ( + + )} + {step === "organization" && } + {step === "name" && ( + { + await complete.mutateAsync(undefined); + setLeaving(true); + }} + /> + )} + {capabilitiesError && !capabilities ? ( +
+

+ We couldn't load your setup. Try again to continue. +

+ +
+ ) : null} + {complete.error ? (

{complete.error.message} @@ -365,24 +418,38 @@ function RouteComponent() { ) : null} {!last && ( )} - {step !== 0 && ( + {step !== "intro" && (