Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Step } from 'react-joyride'

export const navTourSteps: Step[] = [
{
target: '[data-item-id="home"]',
target: '[data-tour="nav-home"]',
title: 'Home',
content:
'Your starting point. Describe what you want to build in plain language or pick a template to get started.',
Expand All @@ -11,62 +11,62 @@ export const navTourSteps: Step[] = [
spotlightPadding: 0,
},
{
target: '[data-item-id="search"]',
target: '[data-tour="nav-search"]',
title: 'Search',
content: 'Quickly find workflows, blocks, and tools. Use Cmd+K to open it from anywhere.',
placement: 'right',
disableBeacon: true,
spotlightPadding: 0,
},
{
target: '[data-item-id="tables"]',
target: '[data-tour="nav-tables"]',
title: 'Tables',
content:
'Store and query structured data. Your workflows can read and write to tables directly.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-item-id="files"]',
target: '[data-tour="nav-files"]',
title: 'Files',
content: 'Upload and manage files that your workflows can process, transform, or reference.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-item-id="knowledge-base"]',
target: '[data-tour="nav-knowledge-base"]',
title: 'Knowledge Base',
content:
'Build knowledge bases from your documents. Set up connectors to give your agents realtime access to your data sources from sources like Notion, Drive, Slack, Confluence, and more.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-item-id="scheduled-tasks"]',
target: '[data-tour="nav-scheduled-tasks"]',
title: 'Scheduled Tasks',
content:
'View and manage background tasks. Set up new tasks, or view the tasks the Mothership is monitoring for upcoming or past executions.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-item-id="logs"]',
target: '[data-tour="nav-logs"]',
title: 'Logs',
content:
'Monitor every workflow execution. See inputs, outputs, errors, and timing for each run. View analytics on performance and costs, filter previous runs, and view snapshots of the workflow at the time of execution.',
placement: 'right',
disableBeacon: true,
},
{
target: '.tasks-section',
target: '[data-tour="nav-tasks"]',
title: 'Tasks',
content:
'Tasks that work for you. Mothership can create, edit, and delete resource throughout the platform. It can also perform actions on your behalf, like sending emails, creating tasks, and more.',
placement: 'right',
disableBeacon: true,
},
{
target: '.workflows-section',
target: '[data-tour="nav-workflows"]',
title: 'Workflows',
content:
'All your workflows live here. Create new ones with the + button and organize them into folders. Deploy your workflows as API, webhook, schedule, or chat widget. Then hit Run to test it out.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,22 @@ export function TourTooltipAdapter({
}
}, [step])

const refCallback = useCallback(
/**
* Forwards the Joyride tooltip ref safely, handling both
* callback refs and RefObject refs from the library.
* Memoized to prevent ref churn (null → node cycling) on re-renders.
*/
const setJoyrideRef = useCallback(
(node: HTMLDivElement | null) => {
if (tooltipProps.ref) {
;(tooltipProps.ref as React.RefCallback<HTMLDivElement>)(node)
const { ref } = tooltipProps
if (!ref) return
if (typeof ref === 'function') {
ref(node)
} else {
;(ref as React.MutableRefObject<HTMLDivElement | null>).current = node
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[tooltipProps.ref]
)

Expand All @@ -86,7 +96,7 @@ export function TourTooltipAdapter({
return (
<>
<div
ref={refCallback}
ref={setJoyrideRef}
role={tooltipProps.role}
aria-modal={tooltipProps['aria-modal']}
style={{ position: 'absolute', opacity: 0, pointerEvents: 'none', width: 0, height: 0 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ function clearTourCompletion(storageKey: string): void {
}
}

/**
* Tracks which tours have already attempted auto-start in this page session.
* Module-level so it survives component remounts (e.g. navigating between
* workflows remounts WorkflowTour), while still resetting on full page reload.
*/
const autoStartAttempted = new Set<string>()

/**
* Shared hook for managing product tour state with smooth transitions.
*
Expand All @@ -87,16 +94,51 @@ export function useTour({
const [isTooltipVisible, setIsTooltipVisible] = useState(true)
const [isEntrance, setIsEntrance] = useState(true)

const hasAutoStarted = useRef(false)
const disabledRef = useRef(disabled)
const retriggerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const transitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const rafRef = useRef<number | null>(null)

useEffect(() => {
disabledRef.current = disabled
}, [disabled])

/**
* Schedules a two-frame rAF to reveal the tooltip after the browser
* finishes repositioning. Stores the outer frame ID in `rafRef` so
* it can be cancelled on unmount or when the tour is interrupted.
*/
const scheduleReveal = useCallback(() => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
}
rafRef.current = requestAnimationFrame(() => {
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null
setIsTooltipVisible(true)
})
})
}, [])

/** Cancels any pending transition timer and rAF reveal */
const cancelPendingTransitions = useCallback(() => {
if (transitionTimerRef.current) {
clearTimeout(transitionTimerRef.current)
transitionTimerRef.current = null
}
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
rafRef.current = null
}
}, [])

const stopTour = useCallback(() => {
cancelPendingTransitions()
setRun(false)
setIsTooltipVisible(true)
setIsEntrance(true)
markTourCompleted(storageKey)
}, [storageKey])
}, [storageKey, cancelPendingTransitions])

/** Transition to a new step with a coordinated fade-out/fade-in */
const transitionToStep = useCallback(
Expand All @@ -106,65 +148,48 @@ export function useTour({
return
}

/** Hide tooltip during transition */
setIsTooltipVisible(false)

if (transitionTimerRef.current) {
clearTimeout(transitionTimerRef.current)
}
cancelPendingTransitions()

transitionTimerRef.current = setTimeout(() => {
transitionTimerRef.current = null
setStepIndex(newIndex)
setIsEntrance(false)

/**
* Wait for the browser to process the Radix Popover repositioning
* before showing the tooltip at the new position.
*/
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setIsTooltipVisible(true)
})
})
scheduleReveal()
}, FADE_OUT_MS)
},
[steps.length, stopTour]
[steps.length, stopTour, cancelPendingTransitions, scheduleReveal]
)

/** Stop the tour when disabled becomes true (e.g. navigating away from the relevant page) */
useEffect(() => {
if (disabled && run) {
cancelPendingTransitions()
setRun(false)
setIsTooltipVisible(true)
setIsEntrance(true)
logger.info(`${tourName} paused — disabled became true`)
}
}, [disabled, run, tourName])
}, [disabled, run, tourName, cancelPendingTransitions])

/** Auto-start on first visit */
/** Auto-start on first visit (once per page session per tour) */
useEffect(() => {
if (disabled || hasAutoStarted.current) return
if (disabled || autoStartAttempted.has(storageKey) || isTourCompleted(storageKey)) return

const timer = setTimeout(() => {
hasAutoStarted.current = true
if (!isTourCompleted(storageKey)) {
setStepIndex(0)
setIsEntrance(true)
setIsTooltipVisible(false)
setRun(true)
logger.info(`Auto-starting ${tourName}`)
if (disabledRef.current) return

requestAnimationFrame(() => {
requestAnimationFrame(() => {
setIsTooltipVisible(true)
})
})
}
autoStartAttempted.add(storageKey)
setStepIndex(0)
setIsEntrance(true)
setIsTooltipVisible(false)
setRun(true)
logger.info(`Auto-starting ${tourName}`)
scheduleReveal()
}, autoStartDelay)

return () => clearTimeout(timer)
}, [storageKey, autoStartDelay, tourName, disabled])
}, [disabled, storageKey, autoStartDelay, tourName, scheduleReveal])

/** Listen for manual trigger events */
useEffect(() => {
Expand All @@ -179,24 +204,14 @@ export function useTour({
clearTimeout(retriggerTimerRef.current)
}

/**
* Start with the tooltip hidden so Joyride can mount, find the
* target element, and position its overlay/spotlight before the
* tooltip card appears.
*/
retriggerTimerRef.current = setTimeout(() => {
retriggerTimerRef.current = null
setStepIndex(0)
setIsEntrance(true)
setIsTooltipVisible(false)
setRun(true)
logger.info(`${tourName} triggered via event`)

requestAnimationFrame(() => {
requestAnimationFrame(() => {
setIsTooltipVisible(true)
})
})
scheduleReveal()
}, 50)
}

Expand All @@ -207,15 +222,17 @@ export function useTour({
clearTimeout(retriggerTimerRef.current)
}
}
}, [triggerEvent, resettable, storageKey, tourName])
}, [triggerEvent, resettable, storageKey, tourName, scheduleReveal])

/** Clean up all pending async work on unmount */
useEffect(() => {
return () => {
if (transitionTimerRef.current) {
clearTimeout(transitionTimerRef.current)
cancelPendingTransitions()
if (retriggerTimerRef.current) {
clearTimeout(retriggerTimerRef.current)
}
}
}, [])
}, [cancelPendingTransitions])

const handleCallback = useCallback(
(data: CallBackProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const workflowTourSteps: Step[] = [
disableBeacon: true,
},
{
target: '[data-tab-button="copilot"]',
target: '[data-tour="tab-copilot"]',
title: 'AI Copilot',
content:
'Build and debug workflows using natural language. Describe what you want and Copilot creates the blocks for you.',
Expand All @@ -19,7 +19,7 @@ export const workflowTourSteps: Step[] = [
spotlightPadding: 0,
},
{
target: '[data-tab-button="toolbar"]',
target: '[data-tour="tab-toolbar"]',
title: 'Block Library',
content:
'Browse all available blocks and triggers. Drag them onto the canvas to build your workflow step by step.',
Expand All @@ -28,7 +28,7 @@ export const workflowTourSteps: Step[] = [
spotlightPadding: 0,
},
{
target: '[data-tab-button="editor"]',
target: '[data-tour="tab-editor"]',
title: 'Block Editor',
content:
'Click any block on the canvas to configure it here. Set inputs, credentials, and fine-tune behavior.',
Expand Down
Loading
Loading