new onboarding flow

This commit is contained in:
tusharmagar 2026-03-11 10:00:36 +05:30 committed by Ramnique Singh
parent 82f9051cb6
commit e27a93d051

View file

@ -2,7 +2,8 @@
import * as React from "react" import * as React from "react"
import { useState, useEffect, useCallback } from "react" import { useState, useEffect, useCallback } from "react"
import { Loader2, Mic, Mail, CheckCircle2, MessageSquare } from "lucide-react" import { Loader2, Mic, Mail, CheckCircle2, ArrowLeft, MessageSquare } from "lucide-react"
// import { MessageSquare } from "lucide-react"
import { import {
Dialog, Dialog,
@ -38,7 +39,7 @@ interface OnboardingModalProps {
onComplete: () => void onComplete: () => void
} }
type Step = 0 | 1 | 2 | 3 type Step = 0 | 1 | 2 | 3 | 4
type OnboardingPath = 'rowboat' | 'byok' | null type OnboardingPath = 'rowboat' | 'byok' | null
@ -367,11 +368,29 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
}, []) }, [])
const handleNext = () => { const handleNext = () => {
if (currentStep < 3) { if (currentStep < 4) {
setCurrentStep((prev) => (prev + 1) as Step) setCurrentStep((prev) => (prev + 1) as Step)
} }
} }
const handleBack = () => {
if (currentStep === 1) {
// BYOK upsell → back to sign-in page
setOnboardingPath(null)
setCurrentStep(0 as Step)
} else if (currentStep === 2) {
// LLM setup → back to BYOK upsell
setCurrentStep(1 as Step)
} else if (currentStep === 3) {
// Connect accounts → back depends on path
if (onboardingPath === 'rowboat') {
setCurrentStep(0 as Step)
} else {
setCurrentStep(2 as Step)
}
}
}
const handleComplete = () => { const handleComplete = () => {
onComplete() onComplete()
} }
@ -486,11 +505,11 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
// Auto-advance from Rowboat sign-in step when OAuth completes // Auto-advance from Rowboat sign-in step when OAuth completes
useEffect(() => { useEffect(() => {
if (onboardingPath !== 'rowboat' || currentStep !== 1) return if (onboardingPath !== 'rowboat' || currentStep !== 0) return
const cleanup = window.ipc.on('oauth:didConnect', (event) => { const cleanup = window.ipc.on('oauth:didConnect', (event) => {
if (event.provider === 'rowboat' && event.success) { if (event.provider === 'rowboat' && event.success) {
setCurrentStep(2 as Step) setCurrentStep(3 as Step)
} }
}) })
@ -568,20 +587,30 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
startConnect('google', clientId) startConnect('google', clientId)
}, [startConnect]) }, [startConnect])
// Step indicator // Step indicator - dynamic based on path
const renderStepIndicator = () => ( const renderStepIndicator = () => {
<div className="flex gap-2 justify-center mb-6"> // Rowboat path: Sign In (0), Connect (3), Done (4) = 3 dots
{[0, 1, 2, 3].map((step) => ( // BYOK path: Sign In (0), Upsell (1), Model (2), Connect (3), Done (4) = 5 dots
<div // Before path is chosen: show 3 dots (minimal)
key={step} const rowboatSteps = [0, 3, 4]
className={cn( const byokSteps = [0, 1, 2, 3, 4]
"w-2 h-2 rounded-full transition-colors", const steps = onboardingPath === 'byok' ? byokSteps : rowboatSteps
currentStep >= step ? "bg-primary" : "bg-muted" const currentIndex = steps.indexOf(currentStep)
)}
/> return (
))} <div className="flex gap-2 justify-center mb-6">
</div> {steps.map((_, i) => (
) <div
key={i}
className={cn(
"w-2 h-2 rounded-full transition-colors",
currentIndex >= i ? "bg-primary" : "bg-muted"
)}
/>
))}
</div>
)
}
// Helper to render an OAuth provider row // Helper to render an OAuth provider row
const renderOAuthProvider = (provider: string, displayName: string, icon: React.ReactNode, description: string) => { const renderOAuthProvider = (provider: string, displayName: string, icon: React.ReactNode, description: string) => {
@ -788,55 +817,15 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
</div> </div>
) )
// Step 0: Path Choice (Rowboat vs BYOK) // Step 0: Sign in to Rowboat (with BYOK option)
const renderPathChoiceStep = () => ( const renderSignInStep = () => {
<div className="flex flex-col">
<div className="flex items-center justify-center gap-3 mb-3">
<span className="text-lg font-medium text-muted-foreground">Your AI coworker, with memory</span>
</div>
<DialogHeader className="text-center mb-6">
<DialogTitle className="text-2xl">Get Started</DialogTitle>
<DialogDescription className="text-base">
Choose how you'd like to set up Rowboat
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 sm:grid-cols-2">
<button
onClick={() => {
setOnboardingPath('rowboat')
setCurrentStep(1 as Step)
}}
className="rounded-lg border-2 border-border hover:border-primary px-6 py-6 text-left transition-colors hover:bg-accent"
>
<div className="text-base font-semibold">Rowboat Account</div>
<div className="text-sm text-muted-foreground mt-2">
Sign in for instant access to all models no API keys needed
</div>
</button>
<button
onClick={() => {
setOnboardingPath('byok')
setCurrentStep(1 as Step)
}}
className="rounded-lg border-2 border-border hover:border-primary px-6 py-6 text-left transition-colors hover:bg-accent"
>
<div className="text-base font-semibold">Bring Your Own Key</div>
<div className="text-sm text-muted-foreground mt-2">
Use your own API keys from OpenAI, Anthropic, Google, and more
</div>
</button>
</div>
</div>
)
// Step 1 (Rowboat path): Sign in to Rowboat
const renderRowboatSignInStep = () => {
const rowboatState = providerStates['rowboat'] || { isConnected: false, isLoading: false, isConnecting: false } const rowboatState = providerStates['rowboat'] || { isConnected: false, isLoading: false, isConnecting: false }
return ( return (
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
<div className="flex items-center justify-center gap-3 mb-3">
<span className="text-lg font-medium text-muted-foreground">Your AI coworker, with memory</span>
</div>
<DialogHeader className="space-y-3 mb-8"> <DialogHeader className="space-y-3 mb-8">
<DialogTitle className="text-2xl">Sign in to Rowboat</DialogTitle> <DialogTitle className="text-2xl">Sign in to Rowboat</DialogTitle>
<DialogDescription className="text-base max-w-md mx-auto"> <DialogDescription className="text-base max-w-md mx-auto">
@ -850,14 +839,17 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
<CheckCircle2 className="size-5" /> <CheckCircle2 className="size-5" />
<span className="text-sm font-medium">Connected to Rowboat</span> <span className="text-sm font-medium">Connected to Rowboat</span>
</div> </div>
<Button onClick={handleNext} size="lg" className="w-full max-w-xs"> <Button onClick={() => setCurrentStep(3 as Step)} size="lg" className="w-full max-w-xs">
Continue Continue
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center gap-4 w-full max-w-xs"> <div className="flex flex-col items-center gap-4 w-full max-w-xs">
<Button <Button
onClick={() => startConnect('rowboat')} onClick={() => {
setOnboardingPath('rowboat')
startConnect('rowboat')
}}
size="lg" size="lg"
className="w-full" className="w-full"
disabled={rowboatState.isConnecting} disabled={rowboatState.isConnecting}
@ -875,11 +867,73 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
)} )}
</div> </div>
)} )}
<div className="w-full flex justify-end mt-8">
<button
onClick={() => {
setOnboardingPath('byok')
setCurrentStep(1 as Step)
}}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Bring your own key
</button>
</div>
</div> </div>
) )
} }
// Step 1 (BYOK path): LLM Setup // Step 1: BYOK upsell — explain benefits of Rowboat before continuing with BYOK
const renderByokUpsellStep = () => (
<div className="flex flex-col">
<DialogHeader className="text-center mb-6">
<DialogTitle className="text-2xl">Before you continue</DialogTitle>
<DialogDescription className="text-base max-w-md mx-auto">
With a Rowboat account, you get:
</DialogDescription>
</DialogHeader>
<div className="space-y-3 mb-8">
<div className="flex items-start gap-3 rounded-md border px-4 py-3">
<CheckCircle2 className="size-5 text-green-600 mt-0.5 shrink-0" />
<div>
<div className="text-sm font-medium">Instant access to all models</div>
<div className="text-xs text-muted-foreground">GPT, Claude, Gemini, and more no separate API keys needed</div>
</div>
</div>
<div className="flex items-start gap-3 rounded-md border px-4 py-3">
<CheckCircle2 className="size-5 text-green-600 mt-0.5 shrink-0" />
<div>
<div className="text-sm font-medium">Simplified billing</div>
<div className="text-xs text-muted-foreground">One account for everything no juggling multiple provider subscriptions</div>
</div>
</div>
<div className="flex items-start gap-3 rounded-md border px-4 py-3">
<CheckCircle2 className="size-5 text-green-600 mt-0.5 shrink-0" />
<div>
<div className="text-sm font-medium">Automatic updates</div>
<div className="text-xs text-muted-foreground">New models are available as soon as they launch, with no configuration changes</div>
</div>
</div>
</div>
<p className="text-sm text-muted-foreground text-center mb-6">
By continuing, you'll set up your own API keys instead of using Rowboat's managed gateway.
</p>
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={handleBack} className="gap-1">
<ArrowLeft className="size-4" />
Back
</Button>
<Button onClick={handleNext}>
I understand
</Button>
</div>
</div>
)
// Step 2 (BYOK path): LLM Setup
const renderLlmSetupStep = () => { const renderLlmSetupStep = () => {
const primaryProviders: Array<{ id: LlmProviderFlavor; name: string; description: string }> = [ const primaryProviders: Array<{ id: LlmProviderFlavor; name: string; description: string }> = [
{ id: "openai", name: "OpenAI", description: "Use your OpenAI API key" }, { id: "openai", name: "OpenAI", description: "Use your OpenAI API key" },
@ -1055,10 +1109,13 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
</div> </div>
)} )}
<div className="flex flex-col gap-3 mt-4"> <div className="flex items-center justify-between mt-4">
<Button variant="ghost" onClick={handleBack} className="gap-1">
<ArrowLeft className="size-4" />
Back
</Button>
<Button <Button
onClick={handleTestAndSaveLlmConfig} onClick={handleTestAndSaveLlmConfig}
size="lg"
disabled={!canTest || testState.status === "testing"} disabled={!canTest || testState.status === "testing"}
> >
{testState.status === "testing" ? ( {testState.status === "testing" ? (
@ -1072,7 +1129,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
) )
} }
// Step 1: Connect Accounts // Step 3: Connect Accounts
const renderAccountConnectionStep = () => ( const renderAccountConnectionStep = () => (
<div className="flex flex-col"> <div className="flex flex-col">
<DialogHeader className="text-center mb-6"> <DialogHeader className="text-center mb-6">
@ -1128,14 +1185,20 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
<Button onClick={handleNext} size="lg"> <Button onClick={handleNext} size="lg">
Continue Continue
</Button> </Button>
<Button variant="ghost" onClick={handleNext} className="text-muted-foreground"> <div className="flex items-center justify-between">
Skip for now <Button variant="ghost" onClick={handleBack} className="gap-1">
</Button> <ArrowLeft className="size-4" />
Back
</Button>
<Button variant="ghost" onClick={handleNext} className="text-muted-foreground">
Skip for now
</Button>
</div>
</div> </div>
</div> </div>
) )
// Step 2: Completion // Step 4: Completion
const renderCompletionStep = () => { const renderCompletionStep = () => {
const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled || gmailConnected const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled || gmailConnected
@ -1224,11 +1287,11 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
onEscapeKeyDown={(e) => e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()}
> >
{renderStepIndicator()} {renderStepIndicator()}
{currentStep === 0 && renderPathChoiceStep()} {currentStep === 0 && renderSignInStep()}
{currentStep === 1 && onboardingPath === 'rowboat' && renderRowboatSignInStep()} {currentStep === 1 && renderByokUpsellStep()}
{currentStep === 1 && onboardingPath === 'byok' && renderLlmSetupStep()} {currentStep === 2 && renderLlmSetupStep()}
{currentStep === 2 && renderAccountConnectionStep()} {currentStep === 3 && renderAccountConnectionStep()}
{currentStep === 3 && renderCompletionStep()} {currentStep === 4 && renderCompletionStep()}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</> </>