Skip to content

Commit a7f344b

Browse files
waleedlatif1adithyaakrishnaclaude
authored
feat(tour): added product tour (#3703)
* feat: add product tour * chore: updated modals * chore: fix the tour * chore: Tour Updates * chore: fix review changes * chore: fix review changes * chore: fix review changes * chore: fix review changes * chore: fix review changes * minor improvements * chore(tour): address PR review comments - Extract shared TourState, TourStateContext, mapPlacement, and TourTooltipAdapter into tour-shared.tsx, eliminating ~100 lines of duplication between product-tour.tsx and workflow-tour.tsx - Fix stale closure in handleStartTour — add isOnWorkflowPage to useCallback deps so Take a tour dispatches the correct event after navigation * chore(tour): address remaining PR review comments - Remove unused logger import and instance in product-tour.tsx - Remove unused tour-tooltip-fade animation from tailwind config - Remove unnecessary overflow-hidden wrapper around WorkflowTour - Add border stroke to arrow SVG in tour-tooltip for visual consistency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(tour): address second round of PR review comments - Remove unnecessary 'use client' from workflow layout (children are already client components) - Fix ref guard timing issue in TourTooltipAdapter that could prevent Joyride from tracking tooltip on subsequent steps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(tour): extract shared Joyride config, fix popover arrow overflow - Extract duplicated Joyride floaterProps/styles into getSharedJoyrideProps() in tour-shared.tsx, parameterized by spotlightBorderRadius - Fix showArrow disabling content scrolling in PopoverContent by wrapping children in a scrollable div when arrow is visible Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * lint * fix(tour): stop running tour when disabled becomes true Prevents nav and workflow tours from overlapping. When a user navigates to a workflow page while the nav tour is running, the disabled flag now stops the nav tour instead of just suppressing auto-start. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tour): move auto-start flag into timer, fix truncate selector conflict - Move hasAutoStarted flag inside setTimeout callback so it's only set when the timer fires, allowing retry if disabled changes during delay - Add data-popover-scroll attribute to showArrow scroll wrapper and exclude it from the flex-1 truncate selector to prevent overflow conflict Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tour): remove duplicate overlay on center-placed tour steps Joyride's spotlight already renders a full-screen overlay via boxShadow. The centered TourTooltip was adding its own bg-black/55 overlay on top, causing double-darkened backgrounds. Removed the redundant overlay div. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: move docs link from settings to help dropdown The Docs link (https://docs.sim.ai) was buried in settings navigation. Moved it to the Help dropdown in the sidebar for better discoverability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Adithya Krishna <aadithya794@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7b6149d commit a7f344b

File tree

22 files changed

+1080
-29
lines changed

22 files changed

+1080
-29
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { NavTour, START_NAV_TOUR_EVENT } from './product-tour'
2+
export { START_WORKFLOW_TOUR_EVENT, WorkflowTour } from './workflow-tour'
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { Step } from 'react-joyride'
2+
3+
export const navTourSteps: Step[] = [
4+
{
5+
target: '[data-item-id="home"]',
6+
title: 'Home',
7+
content:
8+
'Your starting point. Describe what you want to build in plain language or pick a template to get started.',
9+
placement: 'right',
10+
disableBeacon: true,
11+
spotlightPadding: 0,
12+
},
13+
{
14+
target: '[data-item-id="search"]',
15+
title: 'Search',
16+
content: 'Quickly find workflows, blocks, and tools. Use Cmd+K to open it from anywhere.',
17+
placement: 'right',
18+
disableBeacon: true,
19+
spotlightPadding: 0,
20+
},
21+
{
22+
target: '[data-item-id="tables"]',
23+
title: 'Tables',
24+
content:
25+
'Store and query structured data. Your workflows can read and write to tables directly.',
26+
placement: 'right',
27+
disableBeacon: true,
28+
},
29+
{
30+
target: '[data-item-id="files"]',
31+
title: 'Files',
32+
content: 'Upload and manage files that your workflows can process, transform, or reference.',
33+
placement: 'right',
34+
disableBeacon: true,
35+
},
36+
{
37+
target: '[data-item-id="knowledge-base"]',
38+
title: 'Knowledge Base',
39+
content:
40+
'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.',
41+
placement: 'right',
42+
disableBeacon: true,
43+
},
44+
{
45+
target: '[data-item-id="scheduled-tasks"]',
46+
title: 'Scheduled Tasks',
47+
content:
48+
'View and manage background tasks. Set up new tasks, or view the tasks the Mothership is monitoring for upcoming or past executions.',
49+
placement: 'right',
50+
disableBeacon: true,
51+
},
52+
{
53+
target: '[data-item-id="logs"]',
54+
title: 'Logs',
55+
content:
56+
'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.',
57+
placement: 'right',
58+
disableBeacon: true,
59+
},
60+
{
61+
target: '.tasks-section',
62+
title: 'Tasks',
63+
content:
64+
'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.',
65+
placement: 'right',
66+
disableBeacon: true,
67+
},
68+
{
69+
target: '.workflows-section',
70+
title: 'Workflows',
71+
content:
72+
'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.',
73+
placement: 'right',
74+
disableBeacon: true,
75+
},
76+
]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client'
2+
3+
import { useMemo } from 'react'
4+
import dynamic from 'next/dynamic'
5+
import { usePathname } from 'next/navigation'
6+
import { navTourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps'
7+
import type { TourState } from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared'
8+
import {
9+
getSharedJoyrideProps,
10+
TourStateContext,
11+
TourTooltipAdapter,
12+
} from '@/app/workspace/[workspaceId]/components/product-tour/tour-shared'
13+
import { useTour } from '@/app/workspace/[workspaceId]/components/product-tour/use-tour'
14+
15+
const Joyride = dynamic(() => import('react-joyride'), {
16+
ssr: false,
17+
})
18+
19+
const NAV_TOUR_STORAGE_KEY = 'sim-nav-tour-completed-v1'
20+
export const START_NAV_TOUR_EVENT = 'start-nav-tour'
21+
22+
export function NavTour() {
23+
const pathname = usePathname()
24+
const isWorkflowPage = /\/w\/[^/]+/.test(pathname)
25+
26+
const { run, stepIndex, tourKey, isTooltipVisible, isEntrance, handleCallback } = useTour({
27+
steps: navTourSteps,
28+
storageKey: NAV_TOUR_STORAGE_KEY,
29+
autoStartDelay: 1200,
30+
resettable: true,
31+
triggerEvent: START_NAV_TOUR_EVENT,
32+
tourName: 'Navigation tour',
33+
disabled: isWorkflowPage,
34+
})
35+
36+
const tourState = useMemo<TourState>(
37+
() => ({
38+
isTooltipVisible,
39+
isEntrance,
40+
totalSteps: navTourSteps.length,
41+
}),
42+
[isTooltipVisible, isEntrance]
43+
)
44+
45+
return (
46+
<TourStateContext.Provider value={tourState}>
47+
<Joyride
48+
key={tourKey}
49+
steps={navTourSteps}
50+
run={run}
51+
stepIndex={stepIndex}
52+
callback={handleCallback}
53+
continuous
54+
disableScrolling
55+
disableScrollParentFix
56+
disableOverlayClose
57+
spotlightPadding={4}
58+
tooltipComponent={TourTooltipAdapter}
59+
{...getSharedJoyrideProps({ spotlightBorderRadius: 8 })}
60+
/>
61+
</TourStateContext.Provider>
62+
)
63+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
'use client'
2+
3+
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
4+
import type { TooltipRenderProps } from 'react-joyride'
5+
import { TourTooltip } from '@/components/emcn'
6+
7+
/** Shared state passed from the tour component to the tooltip adapter via context */
8+
export interface TourState {
9+
isTooltipVisible: boolean
10+
isEntrance: boolean
11+
totalSteps: number
12+
}
13+
14+
export const TourStateContext = createContext<TourState>({
15+
isTooltipVisible: true,
16+
isEntrance: true,
17+
totalSteps: 0,
18+
})
19+
20+
/**
21+
* Maps Joyride placement strings to TourTooltip placement values.
22+
*/
23+
function mapPlacement(placement?: string): 'top' | 'right' | 'bottom' | 'left' | 'center' {
24+
switch (placement) {
25+
case 'top':
26+
case 'top-start':
27+
case 'top-end':
28+
return 'top'
29+
case 'right':
30+
case 'right-start':
31+
case 'right-end':
32+
return 'right'
33+
case 'bottom':
34+
case 'bottom-start':
35+
case 'bottom-end':
36+
return 'bottom'
37+
case 'left':
38+
case 'left-start':
39+
case 'left-end':
40+
return 'left'
41+
case 'center':
42+
return 'center'
43+
default:
44+
return 'bottom'
45+
}
46+
}
47+
48+
/**
49+
* Adapter that bridges Joyride's tooltip render props to the EMCN TourTooltip component.
50+
* Reads transition state from TourStateContext to coordinate fade animations.
51+
*/
52+
export function TourTooltipAdapter({
53+
step,
54+
index,
55+
isLastStep,
56+
tooltipProps,
57+
primaryProps,
58+
backProps,
59+
closeProps,
60+
}: TooltipRenderProps) {
61+
const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext)
62+
const [targetEl, setTargetEl] = useState<HTMLElement | null>(null)
63+
64+
useEffect(() => {
65+
const { target } = step
66+
if (typeof target === 'string') {
67+
setTargetEl(document.querySelector<HTMLElement>(target))
68+
} else if (target instanceof HTMLElement) {
69+
setTargetEl(target)
70+
} else {
71+
setTargetEl(null)
72+
}
73+
}, [step])
74+
75+
const refCallback = useCallback(
76+
(node: HTMLDivElement | null) => {
77+
if (tooltipProps.ref) {
78+
;(tooltipProps.ref as React.RefCallback<HTMLDivElement>)(node)
79+
}
80+
},
81+
[tooltipProps.ref]
82+
)
83+
84+
const placement = mapPlacement(step.placement)
85+
86+
return (
87+
<>
88+
<div
89+
ref={refCallback}
90+
role={tooltipProps.role}
91+
aria-modal={tooltipProps['aria-modal']}
92+
style={{ position: 'absolute', opacity: 0, pointerEvents: 'none', width: 0, height: 0 }}
93+
/>
94+
<TourTooltip
95+
title={step.title as string}
96+
description={step.content}
97+
step={index + 1}
98+
totalSteps={totalSteps}
99+
placement={placement}
100+
targetEl={targetEl}
101+
isFirst={index === 0}
102+
isLast={isLastStep}
103+
isVisible={isTooltipVisible}
104+
isEntrance={isEntrance && index === 0}
105+
onNext={primaryProps.onClick as () => void}
106+
onBack={backProps.onClick as () => void}
107+
onClose={closeProps.onClick as () => void}
108+
/>
109+
</>
110+
)
111+
}
112+
113+
const SPOTLIGHT_TRANSITION =
114+
'top 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), left 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), width 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), height 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)'
115+
116+
/**
117+
* Returns the shared Joyride floaterProps and styles config used by both tours.
118+
* Only `spotlightPadding` and spotlight `borderRadius` differ between tours.
119+
*/
120+
export function getSharedJoyrideProps(overrides: { spotlightBorderRadius: number }) {
121+
return {
122+
floaterProps: {
123+
disableAnimation: true,
124+
hideArrow: true,
125+
styles: {
126+
floater: {
127+
filter: 'none',
128+
opacity: 0,
129+
pointerEvents: 'none' as React.CSSProperties['pointerEvents'],
130+
width: 0,
131+
height: 0,
132+
},
133+
},
134+
},
135+
styles: {
136+
options: {
137+
zIndex: 10000,
138+
},
139+
spotlight: {
140+
backgroundColor: 'transparent',
141+
border: '1px solid rgba(255, 255, 255, 0.1)',
142+
borderRadius: overrides.spotlightBorderRadius,
143+
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.55)',
144+
position: 'fixed' as React.CSSProperties['position'],
145+
transition: SPOTLIGHT_TRANSITION,
146+
},
147+
overlay: {
148+
backgroundColor: 'transparent',
149+
mixBlendMode: 'unset' as React.CSSProperties['mixBlendMode'],
150+
position: 'fixed' as React.CSSProperties['position'],
151+
height: '100%',
152+
overflow: 'visible',
153+
pointerEvents: 'none' as React.CSSProperties['pointerEvents'],
154+
},
155+
},
156+
} as const
157+
}

0 commit comments

Comments
 (0)