diff --git a/frontend/src/app/forms/stepper-form.tsx b/frontend/src/app/forms/stepper-form.tsx index d69581cf0c..3493578e99 100644 --- a/frontend/src/app/forms/stepper-form.tsx +++ b/frontend/src/app/forms/stepper-form.tsx @@ -13,6 +13,7 @@ import { type MutableRefObject, type ReactNode, useContext, + useLayoutEffect, useRef, useState, } from "react"; @@ -34,6 +35,7 @@ export type StepConfirm> = ( type Step = Stepperize.Step & { assist?: boolean; + description?: string; schema: z.ZodSchema | ((values: Record) => z.ZodSchema); next?: string; previous?: string; @@ -119,6 +121,7 @@ type StepperFormProps = StepperProps & initialStep?: Steps[number]["id"]; controls?: ReactNode; children?: ReactNode; + header?: ReactNode; formId?: string; className?: string; }; @@ -153,23 +156,43 @@ export function StepperForm({ ); } -function useStepperDirection(stepper: { - all: { id: string }[]; - current: { id: string }; -}) { - const currentStepIndex = stepper.all.findIndex( - (s) => s.id === stepper.current.id, - ); - const prevStepIndexRef = useRef(currentStepIndex); - const directionRef = useRef(0); +// Cross-fade between steps. The step container clips overflow so it can animate +// its height between differently-sized steps; a horizontal slide would be cut +// off by that same clip, so the transition is opacity-only and the height +// reveal carries the motion. +const slideVariants = { + enter: { opacity: 0 }, + center: { opacity: 1 }, + exit: { opacity: 0 }, +}; - if (currentStepIndex !== prevStepIndexRef.current) { - directionRef.current = - currentStepIndex > prevStepIndexRef.current ? 1 : -1; - prevStepIndexRef.current = currentStepIndex; - } +// Animates its own height to match the measured height of the current child so +// stepping between steps of different sizes glides instead of snapping. The +// inner content height is measured continuously; the wrapper animates to it and +// clips overflow during the transition. +function AnimatedHeight({ children }: { children: ReactNode }) { + const innerRef = useRef(null); + const [height, setHeight] = useState("auto"); + + useLayoutEffect(() => { + const el = innerRef.current; + if (!el) return; + const measure = () => setHeight(el.offsetHeight); + measure(); + const observer = new ResizeObserver(measure); + observer.observe(el); + return () => observer.disconnect(); + }, []); - return directionRef.current; + return ( + +
{children}
+
+ ); } function getNextVisibleStepId( @@ -211,6 +234,7 @@ function Content({ initialStep, controls, formId, + header, extraChildren, ...formProps }: StepperFormProps & { extraChildren?: ReactNode }) { @@ -246,7 +270,6 @@ function Content({ }); const ref = useRef> | null>({}); - const direction = useStepperDirection(stepper); const formRef = useRef(null); const getValues = () => { @@ -308,7 +331,6 @@ function Content({ if (singlePage) { const step = stepper.current; - const isLast = isLastVisible(step.id); const hasPrev = getPrevVisibleStepId(allSteps as Step[], step.id, getValues()) !== null; @@ -320,6 +342,7 @@ function Content({ }} > + {header}
{ @@ -328,23 +351,29 @@ function Content({ }} className="space-y-6" > - - + + +

{step.title}

+ {(step as Step).description ? ( +

+ {(step as Step).description} +

+ ) : null}
Stepper={Stepper} @@ -364,12 +393,12 @@ function Content({ (step.showPrevious ?? true) && hasPrev } - isLastVisible={isLast} />
{extraChildren}
+
@@ -421,9 +450,6 @@ function Content({ steps.length - 1 === index } controls={controls} - isLastVisible={isLastVisible( - step.id, - )} /> ) : ( stepper.when(step.id, (step) => { @@ -450,9 +476,6 @@ function Content({ step.showPrevious ?? true } - isLastVisible={isLastVisible( - step.id, - )} /> ); }) @@ -477,7 +500,6 @@ function StepPanel({ showNext = true, showPrevious = true, showControls = true, - isLastVisible = false, controls, }: Pick, "Stepper" | "content"> & { stepper: Stepperize.Stepper; @@ -487,7 +509,6 @@ function StepPanel({ showControls?: boolean; showNext?: boolean; showPrevious?: boolean; - isLastVisible?: boolean; controls?: ReactNode; }) { const form = useFormContext(); diff --git a/frontend/src/app/getting-started.tsx b/frontend/src/app/getting-started.tsx index 60c4a1aecf..16ee41fc46 100644 --- a/frontend/src/app/getting-started.tsx +++ b/frontend/src/app/getting-started.tsx @@ -1,13 +1,9 @@ import { Accordion, AccordionContent } from "@radix-ui/react-accordion"; import { faArrowRight, - faBroadcastTower, + faCheck, faCopy, - faDatabase, - faDiagramProject, - faLayerGroup, - faMagnifyingGlass, - faPlug, + faSpinnerThird, Icon, } from "@rivet-gg/icons"; import { deployOptions, type Provider } from "@rivetkit/shared-data"; @@ -19,8 +15,8 @@ import { useSuspenseQuery, } from "@tanstack/react-query"; import { Link, useNavigate, useParams, useRouter } from "@tanstack/react-router"; -import { AnimatePresence, motion } from "framer-motion"; -import { type ReactNode, Suspense, useEffect, useMemo, useState } from "react"; +import { motion } from "framer-motion"; +import { type ReactNode, Suspense, useEffect, useMemo } from "react"; import { useFormContext, useWatch } from "react-hook-form"; import { toast } from "sonner"; import { match } from "ts-pattern"; @@ -37,9 +33,7 @@ import { CodePreview, DiscreteCopyButton, FormField, - Ping, Skeleton, - useInterval, } from "@/components"; import { useCloudNamespaceDataProvider, @@ -72,31 +66,36 @@ import { Content } from "./layout"; const stepper = defineStepper( { id: "install", - title: "Install RivetKit & Skills", + title: "Install RivetKit", + description: + "Add the Rivet skill so your coding agent knows Rivet, then install the package.", + next: "Continue", schema: z.object({}), group: "local", }, { id: "run", - title: "Run locally", - schema: z.object({}), - group: "local", - }, - { - id: "explore", - title: "Explore Rivet Actors", + title: "Build your first Actor", + description: + "Let your coding agent scaffold an Actor, or follow the quickstart yourself.", + next: "Continue", schema: z.object({}), group: "local", }, { id: "provider", - title: "Ready to deploy?", + title: "Where do you want to deploy?", + description: + "Pick a backend. Rivet manages actor orchestration, state, and scaling for you.", + next: "Continue", schema: z.object({ provider: z.string().nonempty() }), group: "deploy", }, { id: "backend", - title: "Connect your Backend", + title: "Connect your backend", + description: + "Deploy your app and connect it so Rivet can route to your Actors.", assist: true, group: "deploy", schema: (values: Record) => { @@ -133,11 +132,13 @@ const stepper = defineStepper( }, { id: "frontend", - title: "Create your first Actor", + title: "Verify deployment", + description: + "We'll detect your first Actor automatically once your backend is live.", assist: true, group: "deploy", schema: z.object({}), - previous: "Edit Provider", + previous: "Edit provider", showNext: false, }, ); @@ -220,19 +221,22 @@ export function GettingStarted({ return ( -
- - +
+
+ + } initialStep={ displayFrontendOnboarding ? "frontend" @@ -252,11 +256,6 @@ export function GettingStarted({ ), - explore: () => ( - - - - ), provider: () => ( @@ -290,15 +289,25 @@ export function GettingStarted({ const provider = (values.provider ?? form.getValues("provider")) as string | undefined; if (stepper.current.id === "provider") { if (features.compute && provider === "rivet") { - await mutateAsyncManagedPool({ - displayName: "default", - pool: "default", - image: null, - maxConcurrentActors: 50_000, - environment: {}, - command: null, - args: [], - }); + try { + await mutateAsyncManagedPool({ + displayName: "default", + pool: "default", + image: null, + maxConcurrentActors: 50_000, + environment: {}, + command: null, + args: [], + }); + } catch (error) { + console.error( + "Failed to create default managed pool during onboarding", + error, + ); + toast.error( + "Couldn't create the default Rivet Compute pool — you can configure it on the next step.", + ); + } } await Promise.all([ @@ -406,60 +415,26 @@ export function GettingStarted({ > - + +
); } -function StepContent({ - children, - wide, -}: { - children: ReactNode; - wide?: boolean; -}) { +function StepContent({ children }: { children: ReactNode }) { return ( - +
{children} - +
); } function StepperFooter() { const s = stepper.useStepper(); - const navigate = useNavigate(); - const skipOnboardingLink = !features.platform ? ( - - ) : null; - - if (s.current.group === "local" && s.current.id !== "explore") { + if (s.current.group === "local") { return (
- {skipOnboardingLink}
); } - if (skipOnboardingLink) { - return ( -
- {skipOnboardingLink} -
- ); - } return null; } +function SkipOnboardingHeaderLink() { + if (features.platform) return null; + return ( +
+ +
+ ); +} + +function OnboardingProgress() { + const s = stepper.useStepper(); + const steps = s.all; + const currentIndex = steps.findIndex((step) => step.id === s.current.id); + const total = steps.length; + const groupLabel = + s.current.group === "local" ? "Local setup" : "Deploy"; + return ( +
+
+ Step {currentIndex + 1} of {total} · {groupLabel} +
+
+ {steps.map((step, i) => ( +
+ ))} +
+
+ ); +} + function ProviderSetup() { const { setValue, control, getValues } = useFormContext(); @@ -504,10 +524,6 @@ function ProviderSetup() { return (
-

- Deploy your application to your preferred backend provider. We - manage the actor orchestration, state, and scaling for you. -

-
-

{option.displayName}

+
+
+

{option.displayName}

+ {option.badge ? ( + + {option.badge} + + ) : null} +

{option.description}

@@ -595,49 +621,70 @@ function ProviderCard({ ); } +function OrDivider({ label }: { label: string }) { + return ( +
+
+ {label} +
+
+ ); +} + +function CommandBox({ command }: { command: string }) { + return ( +
+ + +
+ ); +} + function InstallStep() { return ( -
-
- - Recommended - -

Install Rivet Skills

-

- Run this command in your coding agent to install Rivet - skills for guided setup and development. -

-
- - +
+
+ +
+

Install the Rivet skill

+

+ Run this in your coding agent. The skill teaches it how to + build and deploy with Rivet, then guides you through the + rest of this setup. +

+
-
- + +
+ +
+

Add the RivetKit package

+

+ Install the library your app imports to define and call + Actors. +

+ +
); @@ -759,15 +806,19 @@ Check the troubleshooting guide at https://rivet.dev/docs/actors/troubleshooting function RunLocallyStep() { return ( -
- +
+ +

Follow the quickstart guide

- Set up a Rivet actor project manually step-by-step. + Build a Rivet Actor project by hand, step by step.

@@ -793,143 +844,6 @@ function StepNumber({ n }: { n: number }) { ); } -const exploreFeatures = [ - { - id: "inspector", - icon: faMagnifyingGlass, - label: "Inspector", - title: "RivetKit Inspector", - description: - "A built-in visual debugger that runs locally. View active actors, monitor connections, and trace every interaction in real-time.", - docsUrl: "https://rivet.dev/docs/actors/debugging", - }, - { - id: "state", - icon: faLayerGroup, - label: "In-memory state", - title: "In-memory State", - description: - "Each actor has its own isolated state co-located with compute for instant reads and writes. Persist with SQLite or BYO database.", - docsUrl: "https://rivet.dev/docs/actors/state", - }, - { - id: "storage", - icon: faDatabase, - label: "Storage", - title: "Built-in Storage", - description: - "Actors have built-in KV storage and SQLite. Browse stored data and watch writes happen live in the inspector.", - docsUrl: "https://rivet.dev/docs/actors/storage", - }, - { - id: "workflows", - icon: faDiagramProject, - label: "Workflows", - title: "Durable Workflows", - description: - "Orchestrate multi-step processes that survive crashes and restarts. Automatic retries and step-through history visualization.", - docsUrl: "https://rivet.dev/docs/actors/workflows", - }, - { - id: "events", - icon: faBroadcastTower, - label: "Events", - title: "Event Streams", - description: - "Actors can broadcast events to connected clients. Real-time bidirectional streaming built in.", - docsUrl: "https://rivet.dev/docs/actors/events", - }, - { - id: "rpcs", - icon: faPlug, - label: "RPCs", - title: "Remote Procedure Calls", - description: - "Call actor methods directly from your client with full type safety. The inspector shows every RPC call, its arguments, and response.", - docsUrl: "https://rivet.dev/docs/actors/rpc", - }, -]; - -const CAROUSEL_INTERVAL = 5000; - -const GIF_SRC = `/onboarding-demo.gif?t=${Date.now()}`; - -function ExploreRivet() { - const [activeIndex, setActiveIndex] = useState(0); - const feature = exploreFeatures[activeIndex]; - - const { reset } = useInterval(() => { - setActiveIndex((prev) => (prev + 1) % exploreFeatures.length); - }, CAROUSEL_INTERVAL); - - return ( -
-
- Rivet Actors demo -
-
- {exploreFeatures.map((f, i) => ( - - ))} -
-
-

- {feature.title} -

-

- {feature.description}{" "} - - Learn more - - -

-
-
- ); -} function buildRivetAgentInstructionsCode({ cloudToken, @@ -1150,23 +1064,42 @@ function CopyAgentInstructionsButton({ provider }: { provider?: Provider }) { return ; } -function AgentPromptBanner({ code }: { code: string }) { +function AgentPromptBanner({ + code, + containsSecret = false, + label = "Have your coding agent complete these steps automatically to deploy to Rivet Compute.", +}: { + code: string; + containsSecret?: boolean; + label?: string; +}) { return ( {deploymentUrl ? ( -
- +
+ Deployment URL
- ) : null} + ) : ( + + )} + ) : ( <> - {deploymentUrl ? ( - - ) : ( - - )} - + {deploymentUrl ? ( + + ) : null} )}
@@ -1673,6 +1619,46 @@ function FrontendSetup() { ); } +function VerifyStatusRow({ + state, + label, + sublabel, +}: { + state: "done" | "active" | "pending"; + label: string; + sublabel?: string; +}) { + return ( +
+
+ {state === "done" ? ( + + ) : state === "active" ? ( + + ) : ( +
+ )} +
+
+

+ {label} +

+ {sublabel ? ( +

{sublabel}

+ ) : null} +
+
+ ); +} + function TroubleshootingSection({ endpoint }: { endpoint: string | null }) { const provider = useWatch({ name: "provider" });