add tour to onboarding

This commit is contained in:
Arjun 2026-07-07 02:22:07 +05:30
parent 84c71b37af
commit 037ba76245
7 changed files with 83 additions and 16 deletions

View file

@ -5252,14 +5252,18 @@ function App() {
checkOnboarding()
}, [])
// Handler for onboarding completion
const handleOnboardingComplete = useCallback(async () => {
// Handler for onboarding completion. When the user accepts the tour offer
// on the final step, hand off to the mascot tour once the modal's exit
// animation has cleared.
const handleOnboardingComplete = useCallback(async (opts?: { startTour?: boolean }) => {
try {
await window.ipc.invoke('onboarding:markComplete', null)
setShowOnboarding(false)
} catch (err) {
console.error('Failed to mark onboarding complete:', err)
setShowOnboarding(false)
}
setShowOnboarding(false)
if (opts?.startTour) {
window.setTimeout(() => setTourActive(true), 400)
}
}, [])
@ -5475,11 +5479,14 @@ function App() {
case 'agents':
openBgTasksView()
break
case 'apps':
openAppsView()
break
case 'workspaces':
knowledgeActions.openWorkspaceAt()
break
}
}, [navigateToView, openEmailView, openMeetingsView, openCodeView, knowledgeActions, openBgTasksView])
}, [navigateToView, openEmailView, openMeetingsView, openCodeView, knowledgeActions, openBgTasksView, openAppsView])
// Handler for when a voice note is created/updated
const handleVoiceNoteCreated = useCallback(async (notePath: string) => {

Binary file not shown.

View file

@ -19,7 +19,7 @@ import { CompletionStep } from "./steps/completion-step"
interface OnboardingModalProps {
open: boolean
onComplete: () => void
onComplete: (opts?: { startTour?: boolean }) => void
}
export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {

View file

@ -1,14 +1,17 @@
import { CheckCircle2 } from "lucide-react"
import { CheckCircle2, Volume2 } from "lucide-react"
import { motion } from "motion/react"
import { Button } from "@/components/ui/button"
import { TalkingHead } from "@/components/talking-head"
import type { OnboardingState } from "../use-onboarding-state"
interface CompletionStepProps {
state: OnboardingState
}
const zeroLevel = () => 0
export function CompletionStep({ state }: CompletionStepProps) {
const { connectedProviders, gmailConnected, googleCalendarConnected, handleComplete } = state
const { connectedProviders, gmailConnected, googleCalendarConnected, handleComplete, handleCompleteWithTour } = state
const hasConnections = connectedProviders.length > 0 || gmailConnected || googleCalendarConnected
return (
@ -37,7 +40,7 @@ export function CompletionStep({ state }: CompletionStepProps) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.35 }}
className="text-base text-muted-foreground leading-relaxed max-w-sm mb-8"
className="text-base text-muted-foreground leading-relaxed max-w-sm mb-6"
>
{hasConnections ? (
<>Give me 30 minutes to build your context graph. I can still help with other things on your computer.</>
@ -52,7 +55,7 @@ export function CompletionStep({ state }: CompletionStepProps) {
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.45 }}
className="w-full max-w-sm rounded-xl border bg-muted/30 p-4 mb-8"
className="w-full max-w-sm rounded-xl border bg-muted/30 p-4 mb-6"
>
<p className="text-sm font-semibold mb-3 text-left">Connected</p>
<div className="space-y-2">
@ -104,18 +107,56 @@ export function CompletionStep({ state }: CompletionStepProps) {
</motion.div>
)}
{/* CTA */}
{/* Captain's tour hand-off */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.55 }}
className="w-full max-w-sm rounded-xl border bg-muted/30 px-5 pt-4 pb-5 mb-4"
>
<div className="flex items-center gap-4">
<motion.div
initial={{ scale: 0, rotate: -12 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: "spring", stiffness: 220, damping: 18, delay: 0.65 }}
className="shrink-0"
>
<TalkingHead ttsState="idle" getLevel={zeroLevel} size={88} hat="captain" />
</motion.div>
<div className="text-left">
<p className="text-base font-semibold mb-1">Want the captain's tour?</p>
<p className="text-sm text-muted-foreground leading-snug">
A one-minute guided voyage through everything you just set up
{hasConnections ? <> perfect while I chart your data</> : null}.
</p>
<p className="mt-1.5 flex items-center gap-1.5 text-xs text-muted-foreground">
<Volume2 className="size-3.5 shrink-0" />
<span>I narrate it out loud, so sound on!</span>
</p>
</div>
</div>
</motion.div>
{/* CTAs */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
transition={{ delay: 0.7 }}
className="flex w-full max-w-xs flex-col items-center gap-2"
>
<Button
onClick={handleComplete}
onClick={handleCompleteWithTour}
size="lg"
className="w-full max-w-xs h-12 text-base font-medium"
className="w-full h-12 text-base font-medium"
>
Start Using Rowboat
Take the 1-minute tour
</Button>
<Button
onClick={handleComplete}
variant="ghost"
className="text-muted-foreground"
>
Skip I'll explore myself
</Button>
</motion.div>
</div>

View file

@ -20,7 +20,7 @@ export interface LlmModelOption {
release_date?: string
}
export function useOnboardingState(open: boolean, onComplete: () => void) {
export function useOnboardingState(open: boolean, onComplete: (opts?: { startTour?: boolean }) => void) {
const [currentStep, setCurrentStep] = useState<Step>(0)
const [onboardingPath, setOnboardingPath] = useState<OnboardingPath>(null)
@ -410,10 +410,17 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
}
}, [currentStep, onboardingPath])
// Kept as no-arg handlers (rather than one that takes options) so the
// completion step can pass them straight to onClick without the mouse
// event leaking in as the options object.
const handleComplete = useCallback(() => {
onComplete()
}, [onComplete])
const handleCompleteWithTour = useCallback(() => {
onComplete({ startTour: true })
}, [onComplete])
// Test the active provider's credentials and persist its config. Returns
// whether it succeeded so callers can decide whether to advance or stay.
const testAndSaveActiveProvider = useCallback(async (): Promise<boolean> => {
@ -743,6 +750,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
handleNext,
handleBack,
handleComplete,
handleCompleteWithTour,
handleSwitchToRowboat,
}
}

View file

@ -14,6 +14,7 @@ import tourClipMeetings from '@/assets/tour/meetings.mp3'
import tourClipCode from '@/assets/tour/code.mp3'
import tourClipKnowledge from '@/assets/tour/knowledge.mp3'
import tourClipAgents from '@/assets/tour/agents.mp3'
import tourClipApps from '@/assets/tour/apps.mp3'
import tourClipWorkspaces from '@/assets/tour/workspaces.mp3'
import tourClipChats from '@/assets/tour/chats.mp3'
import tourClipComposer from '@/assets/tour/composer.mp3'
@ -26,6 +27,7 @@ export type TourNavTarget =
| 'code'
| 'knowledge'
| 'agents'
| 'apps'
| 'workspaces'
type TourStep = {
@ -102,6 +104,13 @@ const TOUR_STEPS: TourStep[] = [
title: 'Background agents',
text: 'Background agents work on schedules — they keep your Brain fresh and take care of recurring tasks while you row elsewhere.',
},
{
id: 'apps',
targetId: 'nav-apps',
navigate: 'apps',
title: 'Apps',
text: 'Apps are little tools Rowboat builds just for you — ask for a dashboard or a tracker in chat, and it docks here, ready whenever you are.',
},
{
id: 'workspaces',
targetId: 'nav-workspaces',
@ -141,6 +150,7 @@ const TOUR_CLIPS: Record<string, string> = {
code: tourClipCode,
knowledge: tourClipKnowledge,
agents: tourClipAgents,
apps: tourClipApps,
workspaces: tourClipWorkspaces,
chats: tourClipChats,
composer: tourClipComposer,

View file

@ -913,6 +913,7 @@ export function SidebarContentPanel({
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
data-tour-id="nav-apps"
isActive={activeNav === 'apps'}
onClick={onOpenApps}
>