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.
+