From 4deabfcff015d43469d5f506c3de67e6818a5cf6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 24 Mar 2026 18:06:31 -0700 Subject: [PATCH 1/2] improvement(tour): fix tour auto-start logic and standardize selectors --- .../components/product-tour/nav-tour-steps.ts | 18 +-- .../components/product-tour/tour-shared.tsx | 27 ++-- .../components/product-tour/use-tour.ts | 118 ++++++++++-------- .../product-tour/workflow-tour-steps.ts | 6 +- .../settings/components/general/general.tsx | 61 ++++----- .../w/[workflowId]/components/panel/panel.tsx | 3 + .../w/components/sidebar/sidebar.tsx | 9 +- .../emcn/components/tooltip/tooltip.tsx | 14 ++- .../components/tour-tooltip/tour-tooltip.tsx | 15 ++- .../public/tooltips/auto-connect-on-drop.mp4 | Bin 16973 -> 86006 bytes 10 files changed, 160 insertions(+), 111 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts index 632d94357d6..67446e4db52 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts @@ -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.', @@ -11,7 +11,7 @@ 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', @@ -19,7 +19,7 @@ export const navTourSteps: Step[] = [ 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.', @@ -27,14 +27,14 @@ export const navTourSteps: Step[] = [ 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.', @@ -42,7 +42,7 @@ export const navTourSteps: Step[] = [ 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.', @@ -50,7 +50,7 @@ export const navTourSteps: Step[] = [ 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.', @@ -58,7 +58,7 @@ export const navTourSteps: Step[] = [ 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.', @@ -66,7 +66,7 @@ export const navTourSteps: Step[] = [ 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.', diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx index b0d7436ac4d..b4c84b1659d 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx @@ -1,6 +1,6 @@ 'use client' -import { createContext, useCallback, useContext, useEffect, useState } from 'react' +import { createContext, useContext, useEffect, useRef, useState } from 'react' import type { TooltipRenderProps } from 'react-joyride' import { TourTooltip } from '@/components/emcn' @@ -60,6 +60,7 @@ export function TourTooltipAdapter({ }: TooltipRenderProps) { const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext) const [targetEl, setTargetEl] = useState(null) + const joyrideRef = useRef(null) useEffect(() => { const { target } = step @@ -72,21 +73,27 @@ export function TourTooltipAdapter({ } }, [step]) - const refCallback = useCallback( - (node: HTMLDivElement | null) => { - if (tooltipProps.ref) { - ;(tooltipProps.ref as React.RefCallback)(node) - } - }, - [tooltipProps.ref] - ) + /** + * Forwards the Joyride tooltip ref safely, handling both + * callback refs and RefObject refs from the library. + */ + const setJoyrideRef = (node: HTMLDivElement | null) => { + joyrideRef.current = node + const { ref } = tooltipProps + if (!ref) return + if (typeof ref === 'function') { + ref(node) + } else { + ;(ref as React.MutableRefObject).current = node + } + } const placement = mapPlacement(step.placement) return ( <>
() + /** * Shared hook for managing product tour state with smooth transitions. * @@ -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 | null>(null) const transitionTimerRef = useRef | null>(null) + const rafRef = useRef(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( @@ -106,65 +148,49 @@ 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 (autoStartAttempted.has(storageKey)) return + autoStartAttempted.add(storageKey) 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 || isTourCompleted(storageKey)) return - requestAnimationFrame(() => { - requestAnimationFrame(() => { - setIsTooltipVisible(true) - }) - }) - } + setStepIndex(0) + setIsEntrance(true) + setIsTooltipVisible(false) + setRun(true) + logger.info(`Auto-starting ${tourName}`) + scheduleReveal() }, autoStartDelay) return () => clearTimeout(timer) - }, [storageKey, autoStartDelay, tourName, disabled]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) /** Listen for manual trigger events */ useEffect(() => { @@ -179,11 +205,6 @@ 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) @@ -191,12 +212,7 @@ export function useTour({ setIsTooltipVisible(false) setRun(true) logger.info(`${tourName} triggered via event`) - - requestAnimationFrame(() => { - requestAnimationFrame(() => { - setIsTooltipVisible(true) - }) - }) + scheduleReveal() }, 50) } @@ -207,15 +223,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) => { diff --git a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts index 4a425558696..cb7105eaf68 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts @@ -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.', @@ -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.', @@ -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.', diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx index 40ff85b8816..0e12ba4f905 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { Camera, Check, Pencil } from 'lucide-react' +import { Camera, Check, Info, Pencil } from 'lucide-react' import Image from 'next/image' import { useRouter } from 'next/navigation' import { @@ -376,20 +376,22 @@ export function General() {
- - - - - -

Automatically connect blocks when dropped near each other

- -
-
+
+ + + + + + +

Automatically connect blocks when dropped near each other

+ +
+
+
- - - - - -

Show error popups on blocks when a workflow run fails

- -
-
+
+ + + + + + +

Show error popups on blocks when a workflow run fails

+ +
+
+
handleTabClick('copilot')} data-tab-button='copilot' + data-tour='tab-copilot' > Copilot @@ -705,6 +706,7 @@ export const Panel = memo(function Panel() { variant={_hasHydrated && activeTab === 'toolbar' ? 'active' : 'ghost'} onClick={() => handleTabClick('toolbar')} data-tab-button='toolbar' + data-tour='tab-toolbar' > Toolbar @@ -717,6 +719,7 @@ export const Panel = memo(function Panel() { variant={_hasHydrated && activeTab === 'editor' ? 'active' : 'ghost'} onClick={() => handleTabClick('editor')} data-tab-button='editor' + data-tour='tab-editor' > Editor diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 7be6c8b4534..d6726e23da4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -215,6 +215,7 @@ const SidebarNavItem = memo(function SidebarNavItem({ @@ -1139,7 +1141,7 @@ export const Sidebar = memo(function Sidebar() { )} > {/* Tasks */} -
+
All tasks
{!isCollapsed && ( @@ -1253,7 +1255,10 @@ export const Sidebar = memo(function Sidebar() {
{/* Workflows */} -
+
Workflows
{!isCollapsed && ( diff --git a/apps/sim/components/emcn/components/tooltip/tooltip.tsx b/apps/sim/components/emcn/components/tooltip/tooltip.tsx index 11814ead423..e0d4b53fa62 100644 --- a/apps/sim/components/emcn/components/tooltip/tooltip.tsx +++ b/apps/sim/components/emcn/components/tooltip/tooltip.tsx @@ -98,6 +98,8 @@ interface PreviewProps { width?: number /** Height of the preview in pixels */ height?: number + /** Whether video should loop */ + loop?: boolean /** Optional additional class names */ className?: string } @@ -115,20 +117,22 @@ const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg', '.mov'] as const * * ``` */ -const Preview = ({ src, alt = '', width = 240, height, className }: PreviewProps) => { +const Preview = ({ src, alt = '', width = 240, height, loop = true, className }: PreviewProps) => { const pathname = src.toLowerCase().split('?')[0].split('#')[0] const isVideo = VIDEO_EXTENSIONS.some((ext) => pathname.endsWith(ext)) return ( -
+
{isVideo ? (