Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -920,3 +920,12 @@ input[type="search"]::-ms-clear {
.react-flow__node[data-parent-node-id] .react-flow__handle {
z-index: 30;
}

.__floater__arrow > span > svg {
fill: #30d158 !important;
}

.__floater__arrow > span > svg > path {
fill: #30d158 !important;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProductTour, resetTourCompletion, START_TOUR_EVENT } from './product-tour'
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
'use client'

import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import dynamic from 'next/dynamic'
import { ACTIONS, type CallBackProps, EVENTS, STATUS } from 'react-joyride'
import { tourSteps } from '@/app/workspace/[workspaceId]/components/product-tour/tour-steps'
import { TourTooltip } from '@/app/workspace/[workspaceId]/components/product-tour/tour-tooltip'
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

const logger = createLogger('ProductTour')

const Joyride = dynamic(() => import('react-joyride'), {
ssr: false,
})

const TOUR_STORAGE_KEY = 'sim-tour-completed-v1'
export const START_TOUR_EVENT = 'start-product-tour'

function isTourCompleted(): boolean {
try {
return localStorage.getItem(TOUR_STORAGE_KEY) === 'true'
} catch {
return false
}
}

function markTourCompleted(): void {
try {
localStorage.setItem(TOUR_STORAGE_KEY, 'true')
} catch {
logger.warn('Failed to persist tour completion to localStorage')
}
}

export function resetTourCompletion(): void {
try {
localStorage.removeItem(TOUR_STORAGE_KEY)
} catch {
logger.warn('Failed to reset tour completion in localStorage')
}
}

export function ProductTour() {
const [run, setRun] = useState(false)
const [stepIndex, setStepIndex] = useState(0)
const [tourKey, setTourKey] = useState(0)

const hasAutoStarted = useRef(false)

useEffect(() => {
if (hasAutoStarted.current) return
hasAutoStarted.current = true

const timer = setTimeout(() => {
if (!isTourCompleted()) {
setStepIndex(0)
setRun(true)
logger.info('Auto-starting product tour for first-time user')
}
}, 1200)

return () => clearTimeout(timer)
}, [])

useEffect(() => {
const handleStartTour = () => {
setRun(false)
resetTourCompletion()

setTourKey((k) => k + 1)
setTimeout(() => {
setStepIndex(0)
setRun(true)
logger.info('Product tour triggered via custom event')
}, 50)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
}

window.addEventListener(START_TOUR_EVENT, handleStartTour)
return () => window.removeEventListener(START_TOUR_EVENT, handleStartTour)
}, [])

const stopTour = useCallback(() => {
setRun(false)
markTourCompleted()
}, [])

const handleCallback = useCallback(
(data: CallBackProps) => {
const { action, index, status, type } = data

if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
stopTour()
logger.info('Product tour ended', { status })
return
}

if (type === EVENTS.STEP_AFTER || type === EVENTS.TARGET_NOT_FOUND) {
if (action === ACTIONS.CLOSE) {
stopTour()
logger.info('Product tour closed by user')
return
}

const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1)

if (type === EVENTS.TARGET_NOT_FOUND) {
logger.info('Tour step target not found, skipping', {
stepIndex: index,
target: tourSteps[index]?.target,
})
}

if (nextIndex >= tourSteps.length || nextIndex < 0) {
stopTour()
return
}

setStepIndex(nextIndex)
}
Comment thread
adithyaakrishna marked this conversation as resolved.
Outdated
},
[stopTour]
)

return (
<Joyride
key={tourKey}
steps={tourSteps}
run={run}
stepIndex={stepIndex}
callback={handleCallback}
continuous
showSkipButton
showProgress
disableScrolling
disableOverlayClose
spotlightPadding={6}
tooltipComponent={TourTooltip}
floaterProps={{
disableAnimation: true,
styles: {
floater: {
filter: 'none',
},
},
}}
styles={{
options: {
zIndex: 10000,
overlayColor: 'rgba(0, 0, 0, 0.5)',
},
spotlight: {
backgroundColor: 'transparent',
border: '1.5px solid rgba(255, 255, 255, 0.15)',
borderRadius: 8,
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)',
},
overlay: {
backgroundColor: 'transparent',
mixBlendMode: 'unset' as React.CSSProperties['mixBlendMode'],
},
}}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Step } from 'react-joyride'

export const tourSteps: Step[] = [
{
target: '[data-tour="home-greeting"]',
title: 'Welcome to Sim',
content:
'This is your home base. From here you can describe what you want to build in plain language, or pick a template to get started.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tour="home-chat-input"]',
title: 'Describe your workflow',
content:
'Type what you want to automate — like "monitor my inbox and summarize new emails." Sim will build an AI workflow for you.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tour="home-templates"]',
title: 'Start from a template',
content:
'Or pick one of these pre-built templates to ship your agent in minutes. Click any card to get started.',
placement: 'top',
disableBeacon: true,
},
{
target: '.sidebar-container',
title: 'Sidebar navigation',
content:
'Access everything from here — workflows, tables, files, knowledge base, and logs. This stays with you across all pages.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-item-id="search"]',
title: 'Search anything',
content: 'Use search (or Cmd+K) to quickly find workflows, blocks, tools, and more.',
placement: 'right',
disableBeacon: true,
},
{
target: '.workflows-section',
title: 'Your workflows',
content:
'All your workflows live here. Create new ones with the + button, organize with folders, and switch between them.',
placement: 'right',
disableBeacon: true,
},
Comment thread
adithyaakrishna marked this conversation as resolved.
Outdated
{
target: '[data-tour="canvas"]',
title: 'The workflow canvas',
content:
'This is where you build visually. Drag blocks onto the canvas and connect them together to create AI workflows.',
placement: 'center',
disableBeacon: true,
},
{
target: '[data-tour="command-list"]',
title: 'Quick actions',
content:
'Use these keyboard shortcuts to get started fast. Try Cmd+K to search for blocks, or Cmd+Y to browse templates.',
placement: 'right',
disableBeacon: true,
},
{
target: '[data-tab-button="toolbar"]',
title: 'Block library',
content:
'The Toolbar is your block library. Drag triggers and blocks onto the canvas to build your workflow step by step.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tab-button="copilot"]',
title: 'AI Copilot',
content:
'Copilot helps you build and debug workflows using natural language. Describe what you want and it creates blocks for you.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tab-button="editor"]',
title: 'Block editor',
content:
'Click any block on the canvas to configure it here — set inputs, credentials, and fine-tune behavior.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tour="deploy-run"]',
title: 'Run and deploy',
content:
'Hit Run to execute your workflow and see results in the terminal below. When ready, Deploy as an API, webhook, schedule, or chat widget.',
placement: 'bottom',
disableBeacon: true,
},
{
target: '[data-tour="workflow-controls"]',
title: 'Canvas controls',
content:
'Switch between pointer and hand mode, undo/redo changes, and fit your canvas to view.',
placement: 'top',
disableBeacon: true,
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client'

import type { TooltipRenderProps } from 'react-joyride'
import { cn } from '@/lib/core/utils/cn'

export function TourTooltip({
continuous,
index,
step,
backProps,
closeProps,
primaryProps,
skipProps,
isLastStep,
tooltipProps,
}: TooltipRenderProps) {
return (
<div
{...tooltipProps}
className={cn(
'w-[340px] rounded-[10px] border border-[var(--border-1)]',
'bg-[var(--surface-1)] shadow-[0_8px_30px_rgba(0,0,0,0.3)]'
)}
>
<div className='px-[20px] pt-[20px] pb-[4px]'>
{step.title && (
<h3 className='font-[480] font-season text-[16px] text-[var(--text-primary)] leading-[120%] tracking-[-0.02em]'>
{step.title as string}
</h3>
)}
</div>
<div className='px-[20px] pt-[8px] pb-[16px]'>
<p className='text-[13.5px] text-[var(--text-secondary)] leading-[160%]'>{step.content}</p>
</div>
<div className='flex items-center justify-between border-[var(--border)] border-t px-[16px] py-[12px]'>
<div className='flex items-center'>
{!isLastStep && (
<button
{...skipProps}
type='button'
className='cursor-pointer text-[12.5px] text-[var(--text-muted)] transition-colors hover:text-[var(--text-secondary)]'
>
Skip tour
</button>
)}
</div>
<div className='flex items-center gap-[6px]'>
{index > 0 && (
<button
{...backProps}
type='button'
className='h-[30px] cursor-pointer rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-3)] px-[12px] text-[12.5px] text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)]'
>
Back
</button>
)}
{continuous ? (
<button
{...primaryProps}
type='button'
className='h-[30px] cursor-pointer rounded-[6px] bg-[var(--brand-tertiary-2,#33c482)] px-[12px] font-medium text-[12.5px] text-white transition-colors hover:opacity-90'
>
{isLastStep ? 'Done' : 'Next'}
</button>
) : (
<button
{...closeProps}
type='button'
className='h-[30px] cursor-pointer rounded-[6px] bg-[var(--brand-tertiary-2,#33c482)] px-[12px] font-medium text-[12.5px] text-white transition-colors hover:opacity-90'
>
Close
</button>
)}
</div>
</div>
</div>
)
}
8 changes: 6 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,14 @@ export function Home({ chatId }: HomeProps = {}) {
return (
<div className='h-full overflow-y-auto bg-[var(--bg)] [scrollbar-gutter:stable]'>
<div className='flex min-h-full flex-col items-center justify-center px-[24px] pb-[2vh]'>
<h1 className='mb-[24px] max-w-[42rem] font-[430] font-season text-[32px] text-[var(--text-primary)] tracking-[-0.02em]'>
<h1
data-tour='home-greeting'
className='mb-[24px] max-w-[42rem] font-[430] font-season text-[32px] text-[var(--text-primary)] tracking-[-0.02em]'
>
What should we get done
{session?.user?.name ? `, ${session.user.name.split(' ')[0]}` : ''}?
</h1>
<div ref={initialViewInputRef} className='w-full'>
<div ref={initialViewInputRef} className='w-full' data-tour='home-chat-input'>
<UserInput
defaultValue={initialPrompt}
onSubmit={handleSubmit}
Expand All @@ -356,6 +359,7 @@ export function Home({ chatId }: HomeProps = {}) {
</div>
<div
ref={templateRef}
data-tour='home-templates'
className='-mt-[30vh] mx-auto w-full max-w-[68rem] px-[16px] pb-[32px] sm:px-[24px] lg:px-[40px]'
>
<TemplatePrompts onSelect={handleSubmit} />
Expand Down
Loading