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 (
+
+
+
+ {kind === 'email' && (
+
+ {[
+ { anim: 'tour-env-left', delay: 0, x: 78, y: 96 },
+ { anim: 'tour-env-right', delay: 1.0, x: 128, y: 102 },
+ { anim: 'tour-env-left', delay: 2.0, x: 104, y: 92 },
+ { anim: 'tour-env-right', delay: 3.0, x: 90, y: 104 },
+ ].map((e, i) => (
+
+ ))}
+
+
+ )}
+
+ {kind === 'meetings' && (
+
+ {/* someone's talking */}
+
+ {[0.9, 1.1, 0.7, 1.3, 0.8].map((dur, i) => (
+
+ ))}
+
+ {/* ...and the notepad writes itself */}
+
+
+ {[64, 52, 60, 44].map((w, i) => (
+
+ ))}
+
+
+
+
+
+ )}
+
+ {kind === 'brain' && (
+
+ {/* constellation assembling above the head */}
+
+ {[
+ 'M 22 58 L 52 24',
+ 'M 52 24 L 88 40',
+ 'M 88 40 L 120 18',
+ 'M 88 40 L 70 66',
+ ].map((d, i) => (
+
+ ))}
+ {[
+ [22, 58], [52, 24], [88, 40], [120, 18], [70, 66],
+ ].map(([cx, cy], i) => (
+
+ ))}
+
+ {/* thought-orbs drifting into the head */}
+ {[
+ { fx: '-120px', fy: '-30px', delay: 0 },
+ { fx: '120px', fy: '-40px', delay: 1.1 },
+ { fx: '-90px', fy: '50px', delay: 2.2 },
+ { fx: '110px', fy: '46px', delay: 3.3 },
+ ].map((o, i) => (
+
+ ))}
+
+ )}
+
+ )
+}
+
+/**
+ * Background-agents vignette: a fleet of tiny mascots rowing across the water
+ * at the bottom of the screen while the big one takes a break.
+ */
+export function AgentsFleet() {
+ return (
+
+
+ {[
+ { size: 46, bottom: 4, dur: 16, delay: 0, dir: 'tour-fleet-right' },
+ { size: 34, bottom: 22, dur: 22, delay: -8, dir: 'tour-fleet-left' },
+ { size: 28, bottom: 14, dur: 27, delay: -3, dir: 'tour-fleet-right' },
+ ].map((b, i) => (
+
+ ))}
+
+ )
+}
+
+function MiniRower({ size, flipped }: { size: number; flipped: boolean }) {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/x/apps/renderer/src/lib/tour-sounds.ts b/apps/x/apps/renderer/src/lib/tour-sounds.ts
index c1cb1a62..cc6bf62d 100644
--- a/apps/x/apps/renderer/src/lib/tour-sounds.ts
+++ b/apps/x/apps/renderer/src/lib/tour-sounds.ts
@@ -65,6 +65,22 @@ export class TourSounds {
osc.stop(ctx.currentTime + 0.3)
}
+ /** Gentle high blip — an email landing in the boat. */
+ ding() {
+ const ctx = this.ensure()
+ if (!ctx || !this.master) return
+ const osc = ctx.createOscillator()
+ osc.type = 'sine'
+ osc.frequency.setValueAtTime(880, ctx.currentTime)
+ osc.frequency.exponentialRampToValueAtTime(1320, ctx.currentTime + 0.05)
+ const gain = ctx.createGain()
+ gain.gain.setValueAtTime(0.08, ctx.currentTime)
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3)
+ osc.connect(gain).connect(this.master)
+ osc.start()
+ osc.stop(ctx.currentTime + 0.35)
+ }
+
/** Little four-note arpeggio for the tour finale. */
fanfare() {
const ctx = this.ensure()