Files
modeling-app/src/routes/Onboarding/utils.tsx
Frank Noirot 2d77aa0d36 Revert "Make onboarding optional, able to be ignored on desktop" (#6610)
Revert "Make onboarding optional, able to be ignored on desktop (#6564)"

This reverts commit 820082d7f2.
2025-04-30 21:58:11 -04:00

195 lines
6.5 KiB
TypeScript

import { useCallback, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { waitFor } from 'xstate'
import { ActionButton } from '@src/components/ActionButton'
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { useNetworkContext } from '@src/hooks/useNetworkContext'
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
import { bracket } from '@src/lib/exampleKcl'
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
import { PATHS } from '@src/lib/paths'
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
import { reportRejection, trap } from '@src/lib/trap'
import { settingsActor } from '@src/lib/singletons'
import { onboardingRoutes } from '@src/routes/Onboarding'
import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { parse, resultIsOk } from '@src/lang/wasm'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import { EXECUTION_TYPE_REAL } from '@src/lib/constants'
export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
// Get the 1-indexed step number of the current onboarding step
function useStepNumber(
slug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
) {
return slug
? slug === onboardingPaths.INDEX
? 1
: onboardingRoutes.findIndex(
(r) => r.path === makeUrlPathRelative(slug)
) + 1
: 1
}
export function useDemoCode() {
const { overallState, immediateState } = useNetworkContext()
useEffect(() => {
async function setCodeToDemoIfNeeded() {
// Don't run if the editor isn't loaded or the code is already the bracket
if (!editorManager.editorView || codeManager.code === bracket) {
return
}
// Don't run if the network isn't healthy or the connection isn't established
if (
overallState === NetworkHealthState.Disconnected ||
overallState === NetworkHealthState.Issue ||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
) {
return
}
const pResult = parse(bracket)
if (trap(pResult) || !resultIsOk(pResult)) {
return Promise.reject(pResult)
}
const ast = pResult.program
await updateModelingState(ast, EXECUTION_TYPE_REAL, {
kclManager: kclManager,
editorManager: editorManager,
codeManager: codeManager,
})
}
setCodeToDemoIfNeeded().catch(reportRejection)
}, [editorManager.editorView, immediateState.type, overallState])
}
export function useNextClick(newStatus: string) {
const filePath = useAbsoluteFilePath()
const navigate = useNavigate()
return useCallback(() => {
settingsActor.send({
type: 'set.app.onboardingStatus',
data: { level: 'user', value: newStatus },
})
navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
}, [filePath, newStatus, settingsActor.send, navigate])
}
export function useDismiss() {
const filePath = useAbsoluteFilePath()
const send = settingsActor.send
const navigate = useNavigate()
const settingsCallback = useCallback(() => {
send({
type: 'set.app.onboardingStatus',
data: { level: 'user', value: 'dismissed' },
})
waitFor(settingsActor, (state) => state.matches('idle'))
.then(() => navigate(filePath))
.catch(reportRejection)
}, [send])
return settingsCallback
}
export function OnboardingButtons({
currentSlug,
className,
dismissClassName,
onNextOverride,
...props
}: {
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
className?: string
dismissClassName?: string
onNextOverride?: () => void
} & React.HTMLAttributes<HTMLDivElement>) {
const dismiss = useDismiss()
const stepNumber = useStepNumber(currentSlug)
const previousStep =
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
const goToPrevious = useNextClick(
onboardingPaths.INDEX + (previousStep?.path ?? '')
)
const nextStep =
!stepNumber || stepNumber === onboardingRoutes.length
? null
: onboardingRoutes[stepNumber]
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
return (
<>
<button
onClick={dismiss}
className={
'group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ' +
dismissClassName
}
data-testid="onboarding-dismiss"
>
<CustomIcon
name="close"
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
/>
<Tooltip position="bottom">
Dismiss <kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
</Tooltip>
</button>
<div
className={'flex items-center justify-between ' + (className ?? '')}
{...props}
>
<ActionButton
Element="button"
onClick={() =>
previousStep?.path || previousStep?.index
? goToPrevious()
: dismiss()
}
iconStart={{
icon: previousStep ? 'arrowLeft' : 'close',
className: 'text-chalkboard-10',
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
}}
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
data-testid="onboarding-prev"
>
{previousStep ? `Back` : 'Dismiss'}
</ActionButton>
{stepNumber !== undefined && (
<p className="font-mono text-xs text-center m-0">
{stepNumber} / {onboardingRoutes.length}
</p>
)}
<ActionButton
autoFocus
Element="button"
onClick={() => {
if (nextStep?.path) {
onNextOverride ? onNextOverride() : goToNext()
} else {
dismiss()
}
}}
iconStart={{
icon: nextStep ? 'arrowRight' : 'checkmark',
bgClassName: 'dark:bg-chalkboard-80',
}}
className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next"
>
{nextStep ? `Next` : 'Finish'}
</ActionButton>
</div>
</>
)
}