refactor onboarding: remove welcome screen, streamline model setup, tighten spacing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Arjun 2026-02-05 23:03:47 +05:30
parent 1cddc705df
commit c4c68d76be
2 changed files with 79 additions and 132 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -2,7 +2,7 @@
import * as React from "react"
import { useState, useEffect, useCallback } from "react"
import { Loader2, Mic, Mail, CheckCircle2, Sailboat, MessageSquare } from "lucide-react"
import { Loader2, Mic, Mail, CheckCircle2, MessageSquare } from "lucide-react"
import {
Dialog,
@ -38,7 +38,7 @@ interface OnboardingModalProps {
onComplete: () => void
}
type Step = 0 | 1 | 2 | 3
type Step = 0 | 1 | 2
type LlmProviderFlavor = "openai" | "anthropic" | "google" | "openrouter" | "aigateway" | "ollama" | "openai-compatible"
@ -68,8 +68,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
status: "idle",
})
const [savingLlmConfig, setSavingLlmConfig] = useState(false)
// OAuth provider states
const [providers, setProviders] = useState<string[]>([])
const [providersLoading, setProvidersLoading] = useState(true)
@ -268,7 +266,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
}, [startSlackConnect])
const handleNext = () => {
if (currentStep < 3) {
if (currentStep < 2) {
setCurrentStep((prev) => (prev + 1) as Step)
}
}
@ -277,24 +275,27 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
onComplete()
}
const handleTestConnection = useCallback(async () => {
const handleTestAndSaveLlmConfig = useCallback(async () => {
if (!canTest) return
setTestState({ status: "testing" })
try {
const apiKey = activeConfig.apiKey.trim() || undefined
const baseURL = activeConfig.baseURL.trim() || undefined
const model = activeConfig.model.trim()
const result = await window.ipc.invoke("models:test", {
const providerConfig = {
provider: {
flavor: llmProvider,
apiKey,
baseURL,
},
model,
})
}
const result = await window.ipc.invoke("models:test", providerConfig)
if (result.success) {
setTestState({ status: "success" })
toast.success("Connection successful")
// Save and continue
await window.ipc.invoke("models:saveConfig", providerConfig)
handleNext()
} else {
setTestState({ status: "error", error: result.error })
toast.error(result.error || "Connection test failed")
@ -304,31 +305,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
setTestState({ status: "error", error: "Connection test failed" })
toast.error("Connection test failed")
}
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, canTest, llmProvider])
const handleSaveLlmConfig = useCallback(async () => {
if (testState.status !== "success") return
setSavingLlmConfig(true)
try {
const apiKey = activeConfig.apiKey.trim() || undefined
const baseURL = activeConfig.baseURL.trim() || undefined
const model = activeConfig.model.trim()
await window.ipc.invoke("models:saveConfig", {
provider: {
flavor: llmProvider,
apiKey,
baseURL,
},
model,
})
setSavingLlmConfig(false)
handleNext()
} catch (error) {
console.error("Failed to save LLM config:", error)
toast.error("Failed to save LLM settings")
setSavingLlmConfig(false)
}
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, handleNext, llmProvider, testState.status])
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, canTest, llmProvider, handleNext])
// Check connection status for all providers
const refreshAllStatuses = useCallback(async () => {
@ -468,7 +445,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
// Step indicator component
const StepIndicator = () => (
<div className="flex gap-2 justify-center mb-6">
{[0, 1, 2, 3].map((step) => (
{[0, 1, 2].map((step) => (
<div
key={step}
className={cn(
@ -604,85 +581,75 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
</div>
)
// Step 0: Welcome
const WelcomeStep = () => (
<div className="flex flex-col items-center text-center">
<div className="flex size-20 items-center justify-center rounded-full bg-primary/10 mb-6">
<Sailboat className="size-10 text-primary" />
</div>
<DialogHeader className="space-y-3">
<DialogTitle className="text-2xl">Your AI coworker, with memory</DialogTitle>
<DialogDescription className="text-base max-w-md mx-auto">
Rowboat connects to your email, calendar, and meetings to help you stay on top of your work.
</DialogDescription>
</DialogHeader>
<div className="mt-8 space-y-3 text-left w-full max-w-sm">
<div className="flex gap-3">
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium">1</div>
<p className="text-sm text-muted-foreground">Syncs with your email, calendar, and meetings</p>
</div>
<div className="flex gap-3">
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium">2</div>
<p className="text-sm text-muted-foreground">Remembers the people and context from your conversations</p>
</div>
<div className="flex gap-3">
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium">3</div>
<p className="text-sm text-muted-foreground">Helps you follow up and never miss what matters</p>
</div>
</div>
<Button onClick={handleNext} size="lg" className="mt-8 w-full max-w-xs">
Get Started
</Button>
</div>
)
// Step 1: LLM Setup
// Step 0: LLM Setup
const LlmSetupStep = () => {
const providerOptions: Array<{ id: LlmProviderFlavor; name: string; description: string }> = [
const [showMoreProviders, setShowMoreProviders] = useState(false)
const primaryProviders: Array<{ id: LlmProviderFlavor; name: string; description: string }> = [
{ id: "openai", name: "OpenAI", description: "Use your OpenAI API key" },
{ id: "anthropic", name: "Anthropic", description: "Use your Anthropic API key" },
{ id: "google", name: "Google", description: "Use your Google AI Studio key" },
{ id: "google", name: "Gemini", description: "Use your Google AI Studio key" },
{ id: "ollama", name: "Ollama (Local)", description: "Run a local model via Ollama" },
]
const moreProviders: Array<{ id: LlmProviderFlavor; name: string; description: string }> = [
{ id: "openrouter", name: "OpenRouter", description: "Access multiple models with one key" },
{ id: "aigateway", name: "AI Gateway (Vercel)", description: "Use Vercel's AI Gateway" },
{ id: "ollama", name: "Ollama (Local)", description: "Run a local model via Ollama" },
{ id: "openai-compatible", name: "OpenAI-Compatible", description: "Local or hosted OpenAI-compatible API" },
]
const isMoreProvider = moreProviders.some(p => p.id === llmProvider)
const modelsForProvider = modelsCatalog[llmProvider] || []
const showModelInput = isLocalProvider || modelsForProvider.length === 0
const renderProviderCard = (provider: { id: LlmProviderFlavor; name: string; description: string }) => (
<button
key={provider.id}
onClick={() => {
setLlmProvider(provider.id)
setTestState({ status: "idle" })
}}
className={cn(
"rounded-md border px-3 py-3 text-left transition-colors",
llmProvider === provider.id
? "border-primary bg-primary/5"
: "border-border hover:bg-accent"
)}
>
<div className="text-sm font-medium">{provider.name}</div>
<div className="text-xs text-muted-foreground mt-1">{provider.description}</div>
</button>
)
return (
<div className="flex flex-col">
<DialogHeader className="text-center mb-6">
<div className="flex items-center justify-center gap-3 mb-3">
<img src="/logo-only.png" alt="Rowboat" className="size-10" />
<span className="text-lg font-medium text-muted-foreground">Your AI coworker, with memory</span>
</div>
<DialogHeader className="text-center mb-3">
<DialogTitle className="text-2xl">Choose your model</DialogTitle>
<DialogDescription className="text-base">
Select your provider and model to power Rowboats AI.
</DialogDescription>
</DialogHeader>
<div className="space-y-5">
<div className="space-y-3">
<div className="space-y-2">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Provider</span>
<div className="grid gap-2 sm:grid-cols-2">
{providerOptions.map((provider) => (
<button
key={provider.id}
onClick={() => {
setLlmProvider(provider.id)
setTestState({ status: "idle" })
}}
className={cn(
"rounded-md border px-3 py-3 text-left transition-colors",
llmProvider === provider.id
? "border-primary bg-primary/5"
: "border-border hover:bg-accent"
)}
>
<div className="text-sm font-medium">{provider.name}</div>
<div className="text-xs text-muted-foreground mt-1">{provider.description}</div>
</button>
))}
{primaryProviders.map(renderProviderCard)}
</div>
{(showMoreProviders || isMoreProvider) ? (
<div className="grid gap-2 sm:grid-cols-2 mt-2">
{moreProviders.map(renderProviderCard)}
</div>
) : (
<button
onClick={() => setShowMoreProviders(true)}
className="text-xs text-muted-foreground hover:text-foreground transition-colors mt-1"
>
More providers...
</button>
)}
</div>
<div className="space-y-2">
@ -750,48 +717,36 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
)}
</div>
<div className="mt-6 flex items-center gap-3">
{testState.status === "error" && (
<div className="mt-4 text-sm text-destructive">
{testState.error || "Connection test failed"}
</div>
)}
<div className="flex flex-col gap-3 mt-4">
<Button
variant="default"
onClick={handleTestConnection}
onClick={handleTestAndSaveLlmConfig}
size="lg"
disabled={!canTest || testState.status === "testing"}
>
{testState.status === "testing" ? (
<Loader2 className="size-4 animate-spin" />
<><Loader2 className="size-4 animate-spin mr-2" />Testing connection...</>
) : (
"Test connection"
"Continue"
)}
</Button>
{testState.status === "success" && (
<span className="text-sm text-green-600">Connected</span>
)}
{testState.status === "error" && (
<span className="text-sm text-destructive">
{testState.error || "Test failed"}
</span>
)}
</div>
<div className="flex flex-col gap-3 mt-8">
<Button
onClick={handleSaveLlmConfig}
size="lg"
disabled={testState.status !== "success" || savingLlmConfig}
>
{savingLlmConfig ? <Loader2 className="size-4 animate-spin" /> : "Continue"}
</Button>
</div>
</div>
)
}
// Step 2: Connect Accounts
// Step 1: Connect Accounts
const AccountConnectionStep = () => (
<div className="flex flex-col">
<DialogHeader className="text-center mb-6">
<DialogTitle className="text-2xl">Connect Your Accounts</DialogTitle>
<DialogDescription className="text-base">
Connect your accounts to start syncing your data. You can always add more later.
Connect your accounts to start syncing your data locally. You can always add more later.
</DialogDescription>
</DialogHeader>
@ -821,13 +776,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
{providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', <Mic className="size-5" />, 'AI meeting transcripts')}
</div>
{/* Team Communication Section */}
<div className="space-y-2">
<div className="px-3">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Team Communication</span>
</div>
{renderSlackRow()}
</div>
</>
)}
</div>
@ -843,7 +791,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
</div>
)
// Step 3: Completion
// Step 2: Completion
const CompletionStep = () => {
const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackConnected
@ -856,7 +804,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
<DialogTitle className="text-2xl">You're All Set!</DialogTitle>
<DialogDescription className="text-base max-w-md mx-auto">
{hasConnections ? (
<>Your workspace will populate over the next ~30 minutes as we sync your data.</>
<>Give me 30 minutes to build your context graph.<br />I can still help with other things on your computer.</>
) : (
<>You can connect your accounts anytime from the sidebar to start syncing data.</>
)}
@ -926,10 +874,9 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
onEscapeKeyDown={(e) => e.preventDefault()}
>
<StepIndicator />
{currentStep === 0 && <WelcomeStep />}
{currentStep === 1 && <LlmSetupStep />}
{currentStep === 2 && <AccountConnectionStep />}
{currentStep === 3 && <CompletionStep />}
{currentStep === 0 && <LlmSetupStep />}
{currentStep === 1 && <AccountConnectionStep />}
{currentStep === 2 && <CompletionStep />}
</DialogContent>
</Dialog>
</>