mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
add tour to onboarding
This commit is contained in:
parent
84c71b37af
commit
037ba76245
7 changed files with 83 additions and 16 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
BIN
apps/x/apps/renderer/src/assets/tour/apps.mp3
Normal file
BIN
apps/x/apps/renderer/src/assets/tour/apps.mp3
Normal file
Binary file not shown.
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -913,6 +913,7 @@ export function SidebarContentPanel({
|
|||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-tour-id="nav-apps"
|
||||
isActive={activeNav === 'apps'}
|
||||
onClick={onOpenApps}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue