diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f37e3322..4a45f419 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -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) => { diff --git a/apps/x/apps/renderer/src/assets/tour/apps.mp3 b/apps/x/apps/renderer/src/assets/tour/apps.mp3 new file mode 100644 index 00000000..b7ae862a Binary files /dev/null and b/apps/x/apps/renderer/src/assets/tour/apps.mp3 differ diff --git a/apps/x/apps/renderer/src/components/onboarding/index.tsx b/apps/x/apps/renderer/src/components/onboarding/index.tsx index 2fb7426e..ae83e4fa 100644 --- a/apps/x/apps/renderer/src/components/onboarding/index.tsx +++ b/apps/x/apps/renderer/src/components/onboarding/index.tsx @@ -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) { diff --git a/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx b/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx index 2f5d550c..53472d12 100644 --- a/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx +++ b/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx @@ -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" >

Connected

@@ -104,18 +107,56 @@ export function CompletionStep({ state }: CompletionStepProps) { )} - {/* CTA */} + {/* Captain's tour hand-off */} + +
+ + + +
+

Want the captain's tour?

+

+ A one-minute guided voyage through everything you just set up + {hasConnections ? <> — perfect while I chart your data : null}. +

+

+ + I narrate it out loud, so sound on! +

+
+
+
+ + {/* CTAs */} +
diff --git a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts index f5e6f213..8a9679be 100644 --- a/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts +++ b/apps/x/apps/renderer/src/components/onboarding/use-onboarding-state.ts @@ -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(0) const [onboardingPath, setOnboardingPath] = useState(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 => { @@ -743,6 +750,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) { handleNext, handleBack, handleComplete, + handleCompleteWithTour, handleSwitchToRowboat, } } diff --git a/apps/x/apps/renderer/src/components/product-tour.tsx b/apps/x/apps/renderer/src/components/product-tour.tsx index b6e5fe8d..ecdaade2 100644 --- a/apps/x/apps/renderer/src/components/product-tour.tsx +++ b/apps/x/apps/renderer/src/components/product-tour.tsx @@ -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 = { code: tourClipCode, knowledge: tourClipKnowledge, agents: tourClipAgents, + apps: tourClipApps, workspaces: tourClipWorkspaces, chats: tourClipChats, composer: tourClipComposer, diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 39a68dcc..b6b42c9e 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -913,6 +913,7 @@ export function SidebarContentPanel({