'use client' import type { StepByStepTourGuide } from './target-registry' import type { StepByStepTourGuideGroup, StepByStepTourTaskId, StepByStepTourTaskView, } from './types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent } from '@langgenius/dify-ui/popover' import { useQuery } from '@tanstack/react-query' import { useAtomValue, useSetAtom } from 'jotai' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { buildIntegrationPath } from '@/app/components/integrations/routes' import { IS_CLOUD_EDITION } from '@/config' import { useDocLink } from '@/context/i18n' import { useModalContextSelector } from '@/context/modal-context' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { currentWorkspaceAtom, isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { usePathname, useRouter } from '@/next/navigation' import { hasPermission } from '@/utils/permission' import { getStepByStepTourPermissionVariant, trackStepByStepTourEvent } from './analytics' import { StepByStepTourCoachmark } from './coachmark' import { FloatingChecklist } from './floating-widget' import { activeStepByStepTourGuideGroupAtom, activeStepByStepTourGuideIndexAtom, activeStepByStepTourGuideIndexesAtom, activeStepByStepTourTaskIdAtom, advanceStepByStepTourGuideAtom, completedStepByStepTourTaskIdsAtom, completeStepByStepTourTaskAtom, resetStepByStepTourSessionAtom, skipStepByStepTourAtom, startStepByStepTourTaskAtom, stepByStepTourEnabledForCurrentWorkspaceAtom, stepByStepTourFirstWorkspaceIdAtom, stepByStepTourSkippedAtom, stepByStepTourSkipRecoveryVisibleAtom, uncompleteStepByStepTourTaskAtom, } from './state' import { useSetStepByStepTourShellMode, useStepByStepTourShellModeValue } from './storage' import { getStepByStepTourGuideInteractionPolicy, getStepByStepTourGuideKind, getStepByStepTourGuides, getStepByStepTourTargetSelector, STEP_BY_STEP_TOUR_TARGETS, } from './target-registry' import { STEP_BY_STEP_TOUR_TASKS } from './tasks' import { useStepByStepTourTarget } from './use-tour-target' type StepByStepTourTask = (typeof STEP_BY_STEP_TOUR_TASKS)[number] const hasCompletedAllStepByStepTourTasks = ( completedTaskIds: StepByStepTourTaskId[], tasks: readonly StepByStepTourTask[], ) => tasks.every((task) => completedTaskIds.includes(task.id)) const isPermissionFallbackGuideGroup = ( guideGroup: StepByStepTourGuideGroup | undefined, ): guideGroup is Extract< StepByStepTourGuideGroup, 'homeNoCreate' | 'integrationLimitedAccess' | 'studioNoCreateEmpty' | 'studioNoCreateWithApps' > => guideGroup === 'homeNoCreate' || guideGroup === 'integrationLimitedAccess' || guideGroup === 'studioNoCreateEmpty' || guideGroup === 'studioNoCreateWithApps' const shouldHideOnPathname = (pathname: string) => pathname.startsWith('/app/') || pathname.includes('/installed/') const isGuideEligibleForPlan = (guide: StepByStepTourGuide, canSetPluginPreferences: boolean) => guide.target !== STEP_BY_STEP_TOUR_TARGETS.integrationUpdateSettings || canSetPluginPreferences const isOptionalGuideTargetAvailable = (guide: StepByStepTourGuide, pathname: string) => { if (!guide.optional) return true if (guide.integrationSection && pathname !== buildIntegrationPath(guide.integrationSection)) return true if (typeof document === 'undefined') return true return Boolean(document.querySelector(getStepByStepTourTargetSelector(guide.target))) } const isElementVerticallyVisible = (element: HTMLElement) => { const rect = element.getBoundingClientRect() const viewportHeight = window.innerHeight || document.documentElement.clientHeight return rect.top >= 0 && rect.bottom <= viewportHeight } const scrollTourTargetIntoView = (element: HTMLElement) => { if (isElementVerticallyVisible(element)) return element.scrollIntoView({ block: 'nearest', behavior: 'auto', }) } const createGuideIndexes = (guides: StepByStepTourGuide[]) => guides.map((_, index) => index) const getActiveGuideIndexes = ( guides: StepByStepTourGuide[], guideIndexes: number[] | undefined, ) => { const fallbackGuideIndexes = createGuideIndexes(guides) if (!guideIndexes?.length) return fallbackGuideIndexes const validGuideIndexes = guideIndexes.filter((index) => index >= 0 && index < guides.length) return validGuideIndexes.length > 0 ? validGuideIndexes : fallbackGuideIndexes } type StepByStepTourMountProps = { className?: string } export default function StepByStepTourMount({ className }: StepByStepTourMountProps) { const router = useRouter() const pathname = usePathname() const docLink = useDocLink() const { t } = useTranslation('common') const currentWorkspace = useAtomValue(currentWorkspaceAtom) const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const hasBlockingModalOpen = useModalContextSelector((state) => state.hasBlockingModalOpen) const { data: systemFeatures } = useQuery(systemFeaturesQueryOptions()) const completedTaskIds = useAtomValue(completedStepByStepTourTaskIdsAtom) const skipped = useAtomValue(stepByStepTourSkippedAtom) const firstWorkspaceId = useAtomValue(stepByStepTourFirstWorkspaceIdAtom) const enabledForCurrentWorkspace = useAtomValue(stepByStepTourEnabledForCurrentWorkspaceAtom) const activeTaskId = useAtomValue(activeStepByStepTourTaskIdAtom) const sessionGuideIndex = useAtomValue(activeStepByStepTourGuideIndexAtom) const sessionGuideGroup = useAtomValue(activeStepByStepTourGuideGroupAtom) const sessionGuideIndexes = useAtomValue(activeStepByStepTourGuideIndexesAtom) const skipRecoveryVisible = useAtomValue(stepByStepTourSkipRecoveryVisibleAtom) const setSkipRecoveryVisible = useSetAtom(stepByStepTourSkipRecoveryVisibleAtom) const advanceGuide = useSetAtom(advanceStepByStepTourGuideAtom) const completeTask = useSetAtom(completeStepByStepTourTaskAtom) const resetSession = useSetAtom(resetStepByStepTourSessionAtom) const patchSkipTour = useSetAtom(skipStepByStepTourAtom) const startTask = useSetAtom(startStepByStepTourTaskAtom) const uncompleteTask = useSetAtom(uncompleteStepByStepTourTaskAtom) const shellMode = useStepByStepTourShellModeValue() const setShellMode = useSetStepByStepTourShellMode() const anchorRef = useRef(null) const lastRequestedIntegrationRouteRef = useRef(undefined) const previousSkippedRef = useRef(skipped) const permissionFallbackAnalyticsKeyRef = useRef(undefined) const shownAnalyticsKeyRef = useRef(undefined) const skipTimeoutRef = useRef(undefined) const stepShownAnalyticsKeyRef = useRef(undefined) const [checklistExiting, setChecklistExiting] = useState(false) const currentWorkspaceId = currentWorkspace.id const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management') const homeGuideGroup: Extract | undefined = canCreateApp ? undefined : 'homeNoCreate' const hasKnowledgeWalkthroughPermissions = hasPermission(workspacePermissionKeys, 'dataset.create_and_management') && hasPermission(workspacePermissionKeys, 'dataset.external.connect') const canSetPluginPreferences = hasPermission( workspacePermissionKeys, 'plugin.plugin_preferences', ) const integrationGuideGroup: | Extract | undefined = isCurrentWorkspaceManager ? undefined : 'integrationLimitedAccess' const hasIntegrationWalkthroughPermissions = !integrationGuideGroup useEffect( () => () => { if (skipTimeoutRef.current) window.clearTimeout(skipTimeoutRef.current) }, [], ) const learnDifyEnabled = systemFeatures?.enable_learn_app ?? true const stepByStepTourFeatureEnabled = Boolean(systemFeatures?.enable_step_by_step_tour) const availableTasks = learnDifyEnabled ? STEP_BY_STEP_TOUR_TASKS : STEP_BY_STEP_TOUR_TASKS.filter((task) => task.id !== 'home') const availableTaskIds = availableTasks.map((task) => task.id) const completedAvailableTaskIds = completedTaskIds.filter((taskId) => availableTaskIds.includes(taskId), ) const allTasksCompleted = hasCompletedAllStepByStepTourTasks(completedTaskIds, availableTasks) const currentTask = availableTasks.find((task) => !completedTaskIds.includes(task.id)) const activeTask = activeTaskId ? availableTasks.find((task) => task.id === activeTaskId) : undefined const activeGuideGroup: StepByStepTourGuideGroup | undefined = activeTask?.id === 'home' ? homeGuideGroup : activeTask?.id === 'integration' ? integrationGuideGroup : sessionGuideGroup const activeGuides = activeTask ? getStepByStepTourGuides(activeTask.id, activeGuideGroup) : [] const activeGuideIndex = sessionGuideIndex ?? 0 const activeGuide = activeGuides[activeGuideIndex] const hasActiveGuide = Boolean(activeTask && activeGuide) const minimized = Boolean(activeTask) || shellMode === 'collapsed' const activeGuideIndexes = activeGuides.length > 0 ? getActiveGuideIndexes(activeGuides, sessionGuideIndexes) .filter((index) => isGuideEligibleForPlan(activeGuides[index]!, canSetPluginPreferences)) .filter((index) => isOptionalGuideTargetAvailable(activeGuides[index]!, pathname)) : [] const activeGuidePlanIndex = activeGuideIndexes.findIndex((index) => index === activeGuideIndex) const activeStepIndex = activeGuideIndexes.length > 0 ? activeGuidePlanIndex === -1 ? activeGuideIndexes.filter((index) => index < activeGuideIndex).length : activeGuidePlanIndex : activeGuideIndex const activeStepTotal = activeGuideIndexes.length || activeGuides.length const activeGuideAnalyticsProperties = activeTask && activeGuide ? { task_id: activeTask.id, guide_id: activeGuide.id, } : undefined const visible = IS_CLOUD_EDITION && stepByStepTourFeatureEnabled && enabledForCurrentWorkspace && (hasActiveGuide || !shouldHideOnPathname(pathname)) const overlayVisible = visible && !hasBlockingModalOpen const completionPromptVisible = visible && allTasksCompleted && !activeTask const checklistMinimized = completionPromptVisible ? false : minimized const expanded = !checklistMinimized const activeTargetElement = useStepByStepTourTarget(activeGuide?.target) const activeGuidePlacement = activeGuide?.target === STEP_BY_STEP_TOUR_TARGETS.studioEmptyLearnDify ? 'top' : activeGuide?.target === STEP_BY_STEP_TOUR_TARGETS.integration ? 'right' : 'bottom' const getPermissionVariant = (taskId: StepByStepTourTaskId) => getStepByStepTourPermissionVariant({ canCreateApp, hasIntegrationWalkthroughPermissions, hasKnowledgeWalkthroughPermissions, taskId, }) const trackTaskCompleted = ( completedTaskIds: StepByStepTourTaskId[], taskId: StepByStepTourTaskId, ) => { const completedAvailableTaskIds = completedTaskIds.filter((completedTaskId) => availableTaskIds.includes(completedTaskId), ) trackStepByStepTourEvent({ action: 'task_completed', task_id: taskId, completed_task_count: completedAvailableTaskIds.length, permission_variant: getPermissionVariant(taskId), task_total: availableTasks.length, }) if (hasCompletedAllStepByStepTourTasks(completedTaskIds, availableTasks)) { trackStepByStepTourEvent({ action: 'tour_completed', completed_task_count: completedAvailableTaskIds.length, task_total: availableTasks.length, }) } } const trackTourSkipped = (completedTaskIds: StepByStepTourTaskId[]) => { trackStepByStepTourEvent({ action: 'tour_skipped', task_id: activeTask?.id, completed_task_count: completedTaskIds.filter((taskId) => availableTaskIds.includes(taskId)) .length, }) } useEffect(() => { if (activeTask?.id !== 'integration' || !activeGuide?.integrationSection) return const activeGuideRoute = buildIntegrationPath(activeGuide.integrationSection) if (pathname === activeGuideRoute) { lastRequestedIntegrationRouteRef.current = undefined return } if (lastRequestedIntegrationRouteRef.current === activeGuideRoute) return lastRequestedIntegrationRouteRef.current = activeGuideRoute router.push(activeGuideRoute) }, [activeGuide?.integrationSection, activeTask?.id, pathname, router]) useEffect(() => { if (!activeTargetElement) return scrollTourTargetIntoView(activeTargetElement) }, [activeGuide?.target, activeTargetElement]) useEffect(() => { if (!visible) return const entryPoint = previousSkippedRef.current ? 'reenabled_after_skip' : firstWorkspaceId === currentWorkspaceId ? 'first_workspace' : 'help_menu_enabled' const shownAnalyticsKey = `${currentWorkspaceId}:${entryPoint}` if (shownAnalyticsKeyRef.current === shownAnalyticsKey) return shownAnalyticsKeyRef.current = shownAnalyticsKey trackStepByStepTourEvent({ action: 'tour_shown', completed_task_count: completedAvailableTaskIds.length, entry_point: entryPoint, task_total: availableTasks.length, }) }, [ availableTasks.length, completedAvailableTaskIds.length, currentWorkspaceId, firstWorkspaceId, visible, ]) useEffect(() => { if (!visible || !activeTask || !activeGuide || !activeTargetElement) return const guideAnalyticsProperties = { task_id: activeTask.id, guide_id: activeGuide.id } const stepShownAnalyticsKey = [ currentWorkspaceId, guideAnalyticsProperties.task_id, guideAnalyticsProperties.guide_id, ].join(':') if (stepShownAnalyticsKeyRef.current === stepShownAnalyticsKey) return stepShownAnalyticsKeyRef.current = stepShownAnalyticsKey trackStepByStepTourEvent({ action: 'guide_shown', task_id: guideAnalyticsProperties.task_id, guide_id: guideAnalyticsProperties.guide_id, }) }, [ activeGuide, activeGuideGroup, activeStepIndex, activeStepTotal, activeTargetElement, activeTask, currentWorkspaceId, visible, ]) useEffect(() => { if (!visible) return if (currentTask?.id === 'knowledge' && !hasKnowledgeWalkthroughPermissions) { const fallbackAnalyticsKey = `${currentWorkspaceId}:knowledge:no_knowledge_permission` if (permissionFallbackAnalyticsKeyRef.current === fallbackAnalyticsKey) return permissionFallbackAnalyticsKeyRef.current = fallbackAnalyticsKey trackStepByStepTourEvent({ action: 'permission_fallback_shown', task_id: 'knowledge', permission_variant: 'no_knowledge_permission', }) return } if (!activeTask) return if (!isPermissionFallbackGuideGroup(activeGuideGroup)) return const fallbackAnalyticsKey = `${currentWorkspaceId}:${activeTask.id}:${activeGuideGroup}` if (permissionFallbackAnalyticsKeyRef.current === fallbackAnalyticsKey) return permissionFallbackAnalyticsKeyRef.current = fallbackAnalyticsKey trackStepByStepTourEvent({ action: 'permission_fallback_shown', task_id: activeTask.id, permission_variant: activeTask.id === 'integration' ? 'no_integration_permission' : 'no_create', }) }, [ activeGuideGroup, activeTask, currentTask?.id, currentWorkspaceId, hasKnowledgeWalkthroughPermissions, visible, ]) useEffect(() => { previousSkippedRef.current = skipped }, [skipped]) if (!visible && !skipRecoveryVisible) return null const title = t(($) => $['stepByStepTour.title']) const taskCopy: Record< StepByStepTourTaskId, Pick > = { home: { title: canCreateApp ? t(($) => $['stepByStepTour.tasks.home.title']) : t(($) => $['stepByStepTour.tasks.home.noCreate.title']), description: canCreateApp ? t(($) => $['stepByStepTour.tasks.home.description']) : t(($) => $['stepByStepTour.tasks.home.noCreate.description']), primaryActionLabel: t(($) => $['stepByStepTour.tasks.home.primaryActionLabel']), }, studio: { title: canCreateApp ? t(($) => $['stepByStepTour.tasks.studio.title']) : t(($) => $['stepByStepTour.tasks.studio.noCreate.title']), description: canCreateApp ? t(($) => $['stepByStepTour.tasks.studio.description']) : t(($) => $['stepByStepTour.tasks.studio.noCreate.description']), primaryActionLabel: t(($) => $['stepByStepTour.tasks.studio.primaryActionLabel']), }, knowledge: { title: hasKnowledgeWalkthroughPermissions ? t(($) => $['stepByStepTour.tasks.knowledge.title']) : t(($) => $['stepByStepTour.tasks.knowledge.noPermission.title']), description: hasKnowledgeWalkthroughPermissions ? t(($) => $['stepByStepTour.tasks.knowledge.description']) : t(($) => $['stepByStepTour.tasks.knowledge.noPermission.description']), primaryActionLabel: hasKnowledgeWalkthroughPermissions ? t(($) => $['stepByStepTour.tasks.knowledge.primaryActionLabel']) : t(($) => $['stepByStepTour.tasks.knowledge.noPermission.primaryActionLabel']), }, integration: { title: hasIntegrationWalkthroughPermissions ? t(($) => $['stepByStepTour.tasks.integration.title']) : t(($) => $['stepByStepTour.tasks.integration.noPermission.title']), description: hasIntegrationWalkthroughPermissions ? t(($) => $['stepByStepTour.tasks.integration.description']) : t(($) => $['stepByStepTour.tasks.integration.noPermission.description']), primaryActionLabel: t(($) => $['stepByStepTour.tasks.integration.primaryActionLabel']), }, } const tasks = availableTasks.map((task): StepByStepTourTaskView => { const completed = completedTaskIds.includes(task.id) const knowledgeUnavailable = task.id === 'knowledge' && !hasKnowledgeWalkthroughPermissions return { ...taskCopy[task.id], id: task.id, iconClassName: knowledgeUnavailable ? 'i-ri-lock-line' : task.iconClassName, status: completed ? 'completed' : task.id === currentTask?.id ? 'current' : 'pending', canToggleCompletion: false, } }) const skipTour = () => { if (checklistExiting) return setChecklistExiting(true) skipTimeoutRef.current = window.setTimeout(() => { patchSkipTour({ onSuccess: trackTourSkipped, onError: () => { setChecklistExiting(false) setSkipRecoveryVisible(false) }, }) setChecklistExiting(false) setSkipRecoveryVisible(true) }, 160) } const skipActiveGuide = () => { const guideAnalyticsProperties = activeGuideAnalyticsProperties if (guideAnalyticsProperties) { trackStepByStepTourEvent({ action: 'guide_skipped', task_id: guideAnalyticsProperties.task_id, guide_id: guideAnalyticsProperties.guide_id, }) } resetSession() setShellMode('expanded') } const getNextVisibleActiveGuideIndex = (startIndex: number) => { if (activeGuideIndexes.length > 0) { let nextGuideIndexes = activeGuideIndexes let nextGuideIndex = nextGuideIndexes.find((index) => index >= startIndex) while (nextGuideIndex !== undefined) { if ( isGuideEligibleForPlan(activeGuides[nextGuideIndex]!, canSetPluginPreferences) && isOptionalGuideTargetAvailable(activeGuides[nextGuideIndex]!, pathname) ) { return { activeGuideIndex: nextGuideIndex, activeGuideIndexes: nextGuideIndexes } } nextGuideIndexes = nextGuideIndexes.filter((index) => index !== nextGuideIndex) nextGuideIndex = nextGuideIndexes.find((index) => index >= startIndex) } return { activeGuideIndex: -1, activeGuideIndexes: nextGuideIndexes } } for (let index = startIndex; index < activeGuides.length; index += 1) { if ( isGuideEligibleForPlan(activeGuides[index]!, canSetPluginPreferences) && isOptionalGuideTargetAvailable(activeGuides[index]!, pathname) ) { return { activeGuideIndex: index, activeGuideIndexes: undefined } } } return { activeGuideIndex: -1, activeGuideIndexes: undefined } } const completeActiveGuide = () => { if (!activeTask || !activeGuide) return if (activeGuide.completionMode === 'external') return const guideAnalyticsProperties = activeGuideAnalyticsProperties if (guideAnalyticsProperties) { trackStepByStepTourEvent({ action: 'guide_completed', task_id: guideAnalyticsProperties.task_id, guide_id: guideAnalyticsProperties.guide_id, }) } if (activeGuideIndex < activeGuides.length - 1) { const nextActiveGuide = getNextVisibleActiveGuideIndex(activeGuideIndex + 1) if (nextActiveGuide.activeGuideIndex === -1) { completeTask({ taskId: activeTask.id, onSuccess: (completedTaskIds) => trackTaskCompleted(completedTaskIds, activeTask.id), }) resetSession() setShellMode('expanded') return } advanceGuide({ guideIndex: nextActiveGuide.activeGuideIndex, guideIndexes: nextActiveGuide.activeGuideIndexes, }) return } completeTask({ taskId: activeTask.id, onSuccess: (completedTaskIds) => trackTaskCompleted(completedTaskIds, activeTask.id), }) resetSession() setShellMode('expanded') } const dismissCompletedTour = () => { patchSkipTour({ onSuccess: trackTourSkipped, }) resetSession() setShellMode('expanded') } const floatingChecklist = ( $['stepByStepTour.duration'])} minimized={checklistMinimized} progress={{ ariaValueText: t(($) => $['stepByStepTour.progressAriaValueText'], { completed: completedAvailableTaskIds.length, total: availableTasks.length, }), completed: completedAvailableTaskIds.length, total: availableTasks.length, }} completionPrompt={ completionPromptVisible ? { label: t(($) => $['stepByStepTour.completion.label']), title: t(($) => $['stepByStepTour.completion.title']), description: t(($) => $['stepByStepTour.completion.description']), dismissLabel: t(($) => $['stepByStepTour.completion.dismiss']), onDismiss: dismissCompletedTour, } : undefined } tasks={tasks} skipLabel={t(($) => $['stepByStepTour.skip'])} minimizeLabel={t(($) => $['stepByStepTour.minimize'])} restoreLabel={t(($) => $['stepByStepTour.restore'])} getTaskCompleteLabel={(taskTitle) => t(($) => $['stepByStepTour.markTaskComplete'], { title: taskTitle }) } getTaskIncompleteLabel={(taskTitle) => t(($) => $['stepByStepTour.markTaskIncomplete'], { title: taskTitle }) } onMinimize={() => { setShellMode('collapsed') }} onRestore={() => { setShellMode('expanded') }} onSkip={skipTour} onCompleteTask={(taskId) => { const guides = getStepByStepTourGuides(taskId, sessionGuideGroup) const hasExternalCompletionGuide = guides.some( (guide) => getStepByStepTourGuideKind(guide) === 'action', ) if (hasExternalCompletionGuide) return completeTask({ taskId, onSuccess: (completedTaskIds) => trackTaskCompleted(completedTaskIds, taskId), }) }} onStartTask={(taskId) => { const task = availableTasks.find((item) => item.id === taskId) if (!task) return const guideGroup = taskId === 'home' ? homeGuideGroup : taskId === 'integration' ? integrationGuideGroup : undefined trackStepByStepTourEvent({ action: 'task_started', task_id: taskId, permission_variant: getPermissionVariant(taskId), }) if (taskId === 'knowledge' && !hasKnowledgeWalkthroughPermissions) { completeTask({ taskId, onSuccess: (completedTaskIds) => trackTaskCompleted(completedTaskIds, taskId), }) resetSession() setShellMode('expanded') return } const guides = getStepByStepTourGuides(taskId, guideGroup) const guideIndexes = createGuideIndexes(guides).filter((index) => isGuideEligibleForPlan(guides[index]!, canSetPluginPreferences), ) startTask({ taskId, guideGroup, guideIndexes: guideIndexes.length > 0 ? guideIndexes : undefined, }) router.push(task.route) }} onUncompleteTask={(taskId) => { uncompleteTask({ taskId, onSuccess: (completedTaskIds) => { trackStepByStepTourEvent({ action: 'task_reopened', task_id: taskId, completed_task_count: completedTaskIds.filter((completedTaskId) => availableTaskIds.includes(completedTaskId), ).length, }) }, }) }} className={cn( 'transition-opacity duration-150 ease-out motion-reduce:transition-none', checklistExiting && 'opacity-0', )} /> ) return (
{overlayVisible && !allTasksCompleted && activeTask && activeGuide && activeTargetElement && ( $[activeGuide.description]), learnMoreHref: activeGuide.learnMoreDocPath ? docLink(activeGuide.learnMoreDocPath) : undefined, learnMoreLabel: t(($) => $[activeGuide.learnMoreLabel]), primaryActionLabel: t(($) => $[activeGuide.primaryActionLabel]), title: t(($) => $[activeGuide.title]), }} targetElement={activeTargetElement} placement={activeGuidePlacement} stepLabel={t(($) => $['stepByStepTour.stepLabel'], { current: activeStepIndex + 1, total: activeStepTotal, })} skipLabel={t(($) => $['stepByStepTour.skip'])} interactionPolicy={getStepByStepTourGuideInteractionPolicy( activeGuide, activeTask.canClickThrough, )} onSkip={skipActiveGuide} onComplete={completeActiveGuide} /> )} {visible && (!allTasksCompleted || completionPromptVisible) && ( ) } function SkipRecoveryPrompt({ dismissLabel, label, message, onDismiss, }: { dismissLabel: string label: string message: string onDismiss: () => void }) { const dismissRef = useRef(null) useEffect(() => { dismissRef.current?.focus({ preventScroll: true }) }, []) return (

{message}

) }