mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-19 18:35:18 +02:00
new onboarding flow
This commit is contained in:
parent
82f9051cb6
commit
e27a93d051
1 changed files with 142 additions and 79 deletions
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue