From 597164d089e449c04cd7ae65fc7fe82a22dbc4a0 Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Thu, 27 Aug 2026 07:32:08 +0100 Subject: [PATCH] Complete onboarding V3 workshop preview Signed-off-by: kenny lopez --- .../onboarding/ui/OnboardingPreviewApp.tsx | 242 ++++-- .../ui/OnboardingPreviewJourney.tsx | 705 ++++++++++++++++++ .../onboarding/ui/OnboardingPreviewShell.tsx | 38 + 3 files changed, 934 insertions(+), 51 deletions(-) create mode 100644 desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx create mode 100644 desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx diff --git a/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx b/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx index 9ba05bc61a..d528149a2b 100644 --- a/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx +++ b/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx @@ -15,9 +15,21 @@ import { ONBOARDING_LANDING_CTA_CLASS, ONBOARDING_PRIMARY_CTA_CLASS, ONBOARDING_SECONDARY_CTA_CLASS, - OnboardingChrome, } from "./OnboardingChrome"; import { OnboardingFooter, OnboardingFooterProvider } from "./OnboardingFooter"; +import { + BackupPasswordPreview, + CommunityChoicePreview, + CommunityConnectingPreview, + CommunityEntryPreview, + CommunityHomePreview, + CommunityProfilePreview, + type CommunityPreviewRoute, + DefaultConfigPreview, + StarterTeamPreview, + WelcomeChannelPreview, +} from "./OnboardingPreviewJourney"; +import { OnboardingPreviewStep } from "./OnboardingPreviewShell"; import { OnboardingSlideTransition } from "./OnboardingSlideTransition"; import { SetupStepPreview } from "./SetupStepPreview"; @@ -28,7 +40,17 @@ type PreviewPage = | "sign-in" | "sign-in-key" | "forgot-password" - | "setup"; + | "backup-options" + | "backup-password" + | "setup" + | "config" + | "community-choice" + | "community-entry" + | "community-connecting" + | "community-profile" + | "starter-team" + | "welcome-channel" + | "community-home"; const EMAIL_SIGNUP_POLICY: JoinPolicy = { ageAttestationRequired: true, @@ -123,33 +145,6 @@ function Landing({ onNavigate }: { onNavigate: (page: PreviewPage) => void }) { ); } -function PreviewStep({ - children, - current = 2, - onBack, - testId, -}: { - children: React.ReactNode; - current?: number; - onBack: () => void; - testId: string; -}) { - return ( -
- - - -
- {children} -
-
-
- ); -} - type PreviewPasswordStrength = "empty" | "weak" | "moderate" | "strong"; const strengthLabelPhraseVariants = { @@ -446,12 +441,18 @@ function EmailSignup({ const [confirmPassword, setConfirmPassword] = React.useState(""); const [ageConfirmed, setAgeConfirmed] = React.useState(false); const [agreementConfirmed, setAgreementConfirmed] = React.useState(false); - // This workshop never creates an account, so it deliberately permits the - // next preview step without collecting real credentials or consent. - const canContinue = true; + const [openPolicyDocument, setOpenPolicyDocument] = React.useState< + "terms" | "privacy" | null + >(null); + const canContinue = + email.trim().includes("@") && + password.length > 0 && + confirmPassword === password && + ageConfirmed && + agreementConfirmed; return ( - + undefined} + onOpenDocument={setOpenPolicyDocument} policy={EMAIL_SIGNUP_POLICY} relayWsUrl="wss://preview.invalid" textTone="foreground" @@ -504,8 +505,35 @@ function EmailSignup({ + {openPolicyDocument ? ( +
+
+

+ {openPolicyDocument === "terms" + ? "Terms of Service" + : "Privacy Policy"} +

+

+ {openPolicyDocument === "terms" + ? "Review the terms that apply when you use Buzz." + : "Review how information is handled when you use Buzz."} +

+ +
+
+ ) : null}
-
+ ); } @@ -517,9 +545,13 @@ function AgentHarnessStep({ onNext: () => void; }) { return ( - + - + ); } @@ -542,7 +574,7 @@ function EmailSignIn({ const canContinue = email.trim().length > 0 && password.length > 0; return ( - + - + ); } @@ -623,7 +655,7 @@ function PasswordReset({ if (submitted) { return ( - @@ -638,12 +670,15 @@ function PasswordReset({ Use the link we sent to {email.trim()} to reset your password.

-
+ ); } return ( - + - + ); } @@ -701,7 +736,10 @@ function KeySignIn({ onContinue: () => void; }) { return ( - + - + ); } @@ -735,6 +773,11 @@ export function OnboardingPreviewApp() { const [passwordResetEmail, setPasswordResetEmail] = React.useState(""); const [setupBackPage, setSetupBackPage] = React.useState("landing"); + const [communityRoute, setCommunityRoute] = + React.useState("join"); + const [communityName, setCommunityName] = React.useState("Block Community"); + const [displayName, setDisplayName] = React.useState(""); + const [hasAvatar, setHasAvatar] = React.useState(false); React.useEffect(() => { if (page === "landing") { @@ -750,6 +793,10 @@ export function OnboardingPreviewApp() { setRun((current) => current + 1); setSignInEmail(""); setSignupPassword(""); + setCommunityRoute("join"); + setCommunityName("Block Community"); + setDisplayName(""); + setHasAvatar(false); }, []); let content: React.ReactNode; @@ -769,7 +816,7 @@ export function OnboardingPreviewApp() { ); } else if (page === "identity-key") { content = ( - setPage("landing")} testId="onboarding-preview-identity-key" > @@ -779,13 +826,37 @@ export function OnboardingPreviewApp() { setSetupBackPage("identity-key"); setPage("setup"); }} - onOpenPasswordBackup={() => undefined} - onShowOptions={() => undefined} + onOpenPasswordBackup={() => setPage("backup-password")} + onShowOptions={() => setPage("backup-options")} optionsExpanded={false} previewMode returningFromSecurity={false} /> - + + ); + } else if (page === "backup-options") { + content = ( + setPage("identity-key")} + testId="onboarding-preview-backup-options" + > + setPage("setup")} + onOpenPasswordBackup={() => setPage("backup-password")} + onShowOptions={() => setPage("backup-options")} + optionsExpanded + previewMode + returningFromSecurity={false} + /> + + ); + } else if (page === "backup-password") { + content = ( + setPage("backup-options")} + onDone={() => setPage("backup-options")} + /> ); } else if (page === "sign-in") { content = ( @@ -821,11 +892,80 @@ export function OnboardingPreviewApp() { }} /> ); - } else { + } else if (page === "setup") { content = ( setPage(setupBackPage)} - onNext={() => undefined} + onNext={() => setPage("config")} + /> + ); + } else if (page === "config") { + content = ( + setPage("setup")} + onNext={() => setPage("community-choice")} + /> + ); + } else if (page === "community-choice") { + content = ( + setPage("config")} + onChoose={(route) => { + setCommunityRoute(route); + setPage("community-entry"); + }} + /> + ); + } else if (page === "community-entry") { + content = ( + setPage("community-choice")} + onContinue={(name) => { + setCommunityName(name); + setPage("community-connecting"); + }} + route={communityRoute} + /> + ); + } else if (page === "community-connecting") { + content = ( + setPage("community-entry")} + onContinue={() => setPage("community-profile")} + /> + ); + } else if (page === "community-profile") { + content = ( + setHasAvatar((current) => !current)} + onBack={() => setPage("community-entry")} + onDisplayNameChange={setDisplayName} + onNext={() => setPage("starter-team")} + /> + ); + } else if (page === "starter-team") { + content = ( + setPage("community-profile")} + onNext={() => setPage("welcome-channel")} + /> + ); + } else if (page === "welcome-channel") { + content = ( + setPage("starter-team")} + onNext={() => setPage("community-home")} + /> + ); + } else { + content = ( + setPage("welcome-channel")} /> ); } diff --git a/desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx b/desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx new file mode 100644 index 0000000000..04ba4503a1 --- /dev/null +++ b/desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx @@ -0,0 +1,705 @@ +import { Check, Plus, Users } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/shared/ui/button"; +import { Card } from "@/shared/ui/card"; +import { Input } from "@/shared/ui/input"; +import { + ONBOARDING_PRIMARY_CTA_CLASS, + ONBOARDING_SECONDARY_CTA_CLASS, +} from "./OnboardingChrome"; +import { OnboardingFooter } from "./OnboardingFooter"; +import { OnboardingPreviewStep } from "./OnboardingPreviewShell"; +import { OnboardingSlideTransition } from "./OnboardingSlideTransition"; + +const FIELD_CLASS = + "h-12 rounded-2xl border-foreground/15 bg-white px-4 text-sm shadow-none"; +const COMMUNITY_OPTION_CLASS = + "w-full max-w-[320px] items-center px-6 py-4 text-center text-sm font-normal leading-6 text-foreground [--buzz-card-textured-min-height:88px] transition-[filter] duration-150 ease-out hover:brightness-[0.98] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/35"; + +function LabeledField({ + id, + label, + onChange, + placeholder, + type = "text", + value, +}: { + id: string; + label: string; + onChange: (value: string) => void; + placeholder: string; + type?: "password" | "text" | "url"; + value: string; +}) { + return ( + + ); +} + +type BackupPhase = "password" | "saved" | "verify" | "verified"; + +export function BackupPasswordPreview({ + onBack, + onDone, +}: { + onBack: () => void; + onDone: () => void; +}) { + const [phase, setPhase] = React.useState("password"); + const [password, setPassword] = React.useState(""); + const [verificationPassword, setVerificationPassword] = React.useState(""); + const heading = + phase === "verified" + ? "Your backup is verified" + : phase === "verify" + ? "That’s your backup file" + : phase === "saved" + ? "Optionally, test your backup" + : "Backup your key with a password"; + const description = + phase === "verified" + ? "Your file and password can restore your identity." + : phase === "verify" + ? "Now enter your password to prove you can unlock it." + : phase === "saved" + ? "Learn how your backup works. Choose the file you just saved and unlock it with your password." + : "Keep the downloaded file private — you need both it and your password to restore your identity. Save the backup password somewhere safe; Buzz cannot reset it if lost."; + + return ( + setPhase("password")} + security + testId="onboarding-preview-backup-password" + > + +
+

{heading}

+

+ {description} +

+
+
+ {phase === "password" ? ( +
+ + +

Buzz identity backup

+

+ Protected with your backup password and ready to save. +

+
+
+ ) : phase === "saved" ? ( + + + + + + + Buzz identity backup saved + + + Keep the file and password somewhere safe. + + + + ) : phase === "verify" ? ( +
+ +

Buzz identity backup

+

+ Selected and ready to unlock. +

+
+ +
+ ) : ( + + + + )} +
+ + {phase === "password" ? ( + + ) : phase === "saved" ? ( + <> + + + + ) : phase === "verify" ? ( + + ) : ( + + )} + +
+
+ ); +} + +export function DefaultConfigPreview({ + onBack, + onNext, +}: { + onBack: () => void; + onNext: () => void; +}) { + const [harness, setHarness] = React.useState("buzz-agent"); + const [provider, setProvider] = React.useState(""); + const [model, setModel] = React.useState(""); + + return ( + + +
+

+ Configure your default model settings +

+

+ This will be set as your default model configuration across Buzz. + You can always change this in your Settings or give specific agents + a different configuration. +

+
+
+
+ + + +
+
+ + + + +
+
+ ); +} + +export type CommunityPreviewRoute = "join" | "create" | "existing"; + +export function CommunityChoicePreview({ + onBack, + onChoose, +}: { + onBack: () => void; + onChoose: (route: CommunityPreviewRoute) => void; +}) { + return ( + + +
+

Join or create a community

+

+ Join with an invite, create your own community, or reconnect one you + already have. +

+
+
+ {( + [ + ["join", "Join a community"], + ["create", "Create a community"], + ["existing", "I already have a community"], + ] as const + ).map(([route, label]) => ( + + + + ))} +
+
+
+ ); +} + +export function CommunityEntryPreview({ + onBack, + onContinue, + route, +}: { + onBack: () => void; + onContinue: (communityName: string) => void; + route: CommunityPreviewRoute; +}) { + const [value, setValue] = React.useState(""); + const isCreate = route === "create"; + const heading = isCreate + ? "Create a community" + : route === "existing" + ? "Reconnect to your community" + : "Join a community"; + + return ( + + +

{heading}

+

+ {isCreate + ? "Choose a name for the space where your people and agents will work together." + : route === "existing" + ? "Enter the community URL or an invite link. Your role will be restored when you connect." + : "Enter the invite link or community URL you received."} +

+
{ + event.preventDefault(); + onContinue( + isCreate ? value.trim() || "Your community" : "Block Community", + ); + }} + > + + + +
+
+ ); +} + +export function CommunityConnectingPreview({ + communityName, + onBack, + onContinue, +}: { + communityName: string; + onBack: () => void; + onContinue: () => void; +}) { + return ( + + + +

Joining {communityName}

+

Connecting securely…

+ + + +
+
+ ); +} + +export function CommunityProfilePreview({ + displayName, + hasAvatar, + onAvatarChange, + onBack, + onDisplayNameChange, + onNext, +}: { + displayName: string; + hasAvatar: boolean; + onAvatarChange: () => void; + onBack: () => void; + onDisplayNameChange: (value: string) => void; + onNext: () => void; +}) { + return ( + + +
+

Build your profile

+

+ Add a name and avatar. They’ll show up on your messages, reactions, + and agent handoffs. +

+
+
+ + +
+ + + +
+
+ ); +} + +const STARTER_TEAM = ["Fizz", "Honey", "Pollen"] as const; + +export function StarterTeamPreview({ + onBack, + onNext, +}: { + onBack: () => void; + onNext: () => void; +}) { + return ( + + +

Meet your starter team

+

+ Buzz lets you bring multiple agents into the same workspace. Your team + will help you get started using Buzz. +

+
+
+ {STARTER_TEAM.map((name) => ( +
+ {`${name} + + {name} + +
+ ))} +
+
+ + + +
+
+ ); +} + +export function WelcomeChannelPreview({ + communityName, + onBack, + onNext, +}: { + communityName: string; + onBack: () => void; + onNext: () => void; +}) { + return ( + + +

+ Your Welcome channel is ready +

+

+ This is your starting place in {communityName}. Your starter team can + help you learn the space and decide what to do next. +

+ +

# Welcome

+

+ Meet your team, start a conversation, or explore your community. +

+
+ {STARTER_TEAM.map((name) => ( + + {name[0]} + + ))} +
+
+ + + +
+
+ ); +} + +export function CommunityHomePreview({ + communityName, + onBack, +}: { + communityName: string; + onBack: () => void; +}) { + return ( + + +

+ {communityName} +

+

Welcome to Buzz

+

+ You’re in the Welcome channel. Start with a message, meet the people + and agents here, or explore the rest of the community. +

+ +

Fizz

+

+ Welcome! I can show you around and help you get started. +

+
+
+
+ ); +} diff --git a/desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx b/desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx new file mode 100644 index 0000000000..384bf1e414 --- /dev/null +++ b/desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx @@ -0,0 +1,38 @@ +import type * as React from "react"; + +import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; +import { OnboardingChrome } from "./OnboardingChrome"; +import { OnboardingFooterProvider } from "./OnboardingFooter"; + +export function OnboardingPreviewStep({ + children, + current = 2, + onBack, + security = false, + testId, +}: { + children: React.ReactNode; + current?: number; + onBack?: () => void; + security?: boolean; + testId: string; +}) { + return ( +
+ + {!security ? : null} + +
+ {children} +
+
+
+ ); +}