From b5217e1b45aa67f9e459f5ea0f37ae7407dd5695 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Thu, 2 Jul 2026 02:50:57 +0530 Subject: [PATCH] dramatic tour 2 --- .../renderer/src/components/product-tour.tsx | 16 +- .../src/components/tour-vignettes.tsx | 262 ++++++++++++++++++ apps/x/apps/renderer/src/lib/tour-sounds.ts | 16 ++ 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 apps/x/apps/renderer/src/components/tour-vignettes.tsx diff --git a/apps/x/apps/renderer/src/components/product-tour.tsx b/apps/x/apps/renderer/src/components/product-tour.tsx index bb4896a4..76e3b612 100644 --- a/apps/x/apps/renderer/src/components/product-tour.tsx +++ b/apps/x/apps/renderer/src/components/product-tour.tsx @@ -3,6 +3,7 @@ import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { Button } from '@/components/ui/button' import { TalkingHead, type MascotHat } from '@/components/talking-head' +import { AgentsFleet, MascotVignette, type TourVignetteKind } from '@/components/tour-vignettes' import { TourSounds } from '@/lib/tour-sounds' import type { TTSState } from '@/hooks/useVoiceTTS' import { cn } from '@/lib/utils' @@ -24,6 +25,8 @@ type TourStep = { navigate?: TourNavTarget /** Costume the mascot wears at this stop. */ hat?: MascotHat + /** Looping animation staged around the mascot while it presents this stop. */ + vignette?: TourVignetteKind title: string text: string } @@ -46,6 +49,7 @@ const TOUR_STEPS: TourStep[] = [ targetId: 'nav-email', navigate: 'email', hat: 'mailcap', + vignette: 'email', title: 'Email', text: 'Read and triage your inbox right here. Rowboat can summarize threads, label messages, and help you draft replies.', }, @@ -54,6 +58,7 @@ const TOUR_STEPS: TourStep[] = [ targetId: 'nav-meetings', navigate: 'meetings', hat: 'headphones', + vignette: 'meetings', title: 'Meetings', text: 'Record or join meetings, and get transcripts and notes automatically — prep briefs show up before your calls, too.', }, @@ -70,6 +75,7 @@ const TOUR_STEPS: TourStep[] = [ targetId: 'nav-knowledge', navigate: 'knowledge', hat: 'gradcap', + vignette: 'brain', title: 'Brain', text: "Brain is your knowledge base — notes, files, and everything Rowboat learns for you, all connected and searchable.", }, @@ -78,6 +84,7 @@ const TOUR_STEPS: TourStep[] = [ targetId: 'nav-agents', navigate: 'agents', hat: 'captain', + vignette: 'agents', title: 'Background agents', text: 'Background agents work on schedules — they keep your Brain fresh and take care of recurring tasks while you row elsewhere.', }, @@ -418,6 +425,8 @@ export function ProductTour({ travelRafRef.current = requestAnimationFrame(frame) }, [cancelTravel, moveMascot, reducedMotion]) + const playDing = useCallback(() => soundsRef.current?.ding(), []) + const finish = useCallback(() => { cancelSpeechRef.current() onCloseRef.current() @@ -610,6 +619,9 @@ export function ProductTour({ className="fixed left-0 top-0 z-[70]" style={{ width: MASCOT_SIZE, pointerEvents: 'none' }} > + {arrived && !reducedMotion && step.vignette && step.vignette !== 'agents' && ( + + )}
@@ -617,7 +629,7 @@ export function ProductTour({
+ {arrived && !reducedMotion && step.vignette === 'agents' && } + {confettiOn && !reducedMotion && } , document.body diff --git a/apps/x/apps/renderer/src/components/tour-vignettes.tsx b/apps/x/apps/renderer/src/components/tour-vignettes.tsx new file mode 100644 index 00000000..909409e7 --- /dev/null +++ b/apps/x/apps/renderer/src/components/tour-vignettes.tsx @@ -0,0 +1,262 @@ +import { useEffect } from 'react' + +export type MascotVignetteKind = 'email' | 'meetings' | 'brain' +export type TourVignetteKind = MascotVignetteKind | 'agents' + +/** + * Little looping "shows" staged around the mascot while it presents a section + * during the product tour. Purely decorative: everything is pointer-events-none + * and rendered on the tour's own layers, never inside the section's real UI. + */ +export function MascotVignette({ kind, playDing }: { kind: MascotVignetteKind; playDing?: () => void }) { + // One round of dings as the first envelopes land, then let the loop run silent + useEffect(() => { + if (kind !== 'email' || !playDing) return + const timers = [900, 1900, 2900].map((ms) => setTimeout(playDing, ms)) + return () => timers.forEach(clearTimeout) + }, [kind, playDing]) + + return ( +