{ const steps: DriveStep[] = [ { popover: { title: 'Build your first flow', description: "Let's create a temperature converter that validates input and converts Celsius to Fahrenheit.", onNextClick: async () => { const emptyFlow: Flow = { summary: '', description: '', value: { modules: [] }, schema: flowJson.schema, path: '', edited_at: '', edited_by: '', archived: false, extra_perms: {} } await initFlow(emptyFlow, flowStore as StateStore, flowStateStore) driver.moveNext() } } }, { element: '#flow-editor-virtual-Input', onHighlighted: async () => { step2Complete = false await wait(DELAY_MEDIUM) triggerPointerDown('#flow-editor-virtual-Input') await wait(DELAY_SHORT) selectionManager.selectId('Input') await wait(200) const overlay = getDriverOverlay() if (overlay) { overlay.style.width = '50%' overlay.style.right = 'auto' overlay.style.left = '0' } const celsiusInput = document.querySelector( 'input[type="number"][placeholder=""]' ) as HTMLInputElement if (celsiusInput) { celsiusInput.value = '' celsiusInput.dispatchEvent(new Event('input', { bubbles: true })) await wait(DELAY_MEDIUM) celsiusInput.value = '2' celsiusInput.dispatchEvent(new Event('input', { bubbles: true })) await wait(400) celsiusInput.value = '25' celsiusInput.dispatchEvent(new Event('input', { bubbles: true })) step2Complete = true } }, popover: { title: 'Set the input', description: 'Every flow starts with input. Here we define a temperature in Celsius.', side: 'bottom', align: 'start', onNextClick: () => { if (!step2Complete) { sendUserToast('Please wait for the input to be filled...', false, [], undefined, 3000) return } driver.moveNext() } } }, { element: '#flow-editor-add-step-0', onHighlighted: async () => { step3Complete = false // Animate cursor to the add step button const button = document.querySelector('#flow-editor-add-step-0') as HTMLElement if (button) { const fakeCursor1 = await createFakeCursorWithStart(null, button, 1.5) await wait(DELAY_SHORT) button.click() fakeCursor1.remove() } const overlay = getDriverOverlay() if (overlay) { overlay.style.display = 'none' } await wait(DELAY_LONG) const spans = Array.from(document.querySelectorAll('span')) const bunSpan = spans.find((span) => span.textContent?.includes('TypeScript (Bun)') ) as HTMLElement if (bunSpan) { // Animate cursor from add step button to TypeScript (Bun) span const fakeCursor2 = await createFakeCursorWithStart(button, bunSpan, 1.5) await wait(DELAY_MEDIUM) fakeCursor2.remove() // Automatically trigger next step after cursor animation await wait(DELAY_SHORT) // Add module with empty summary and empty content const moduleData = flowJson.value.modules[0] const module: FlowModule = { id: moduleData.id, summary: '', // Start with empty summary value: moduleData.value } // Clear content after module creation if it's a rawscript if ('content' in module.value) { module.value = { ...module.value, content: '' } as typeof module.value } await addModuleToFlow(module) await wait(700) // Restore overlay const overlay = getDriverOverlay() if (overlay) { overlay.style.display = '' } step3Complete = true driver.moveNext() } }, popover: { title: 'Choose TypeScript', description: 'Pick TypeScript (Bun) to write our validation script.', side: 'top', onNextClick: () => { if (!step3Complete) { sendUserToast( 'Please wait for the script to be created...', false, [], undefined, 3000 ) return } driver.moveNext() }, onPrevClick: () => { sendUserToast('Previous is not available for this step', true, [], undefined, 3000) } } }, { element: '#a', onHighlighted: async () => { // Reset the flag when step starts step4Complete = false selectionManager.selectId('a') await wait(DELAY_LONG) const overlay = getDriverOverlay() if (overlay) { overlay.style.width = '50%' overlay.style.right = 'auto' overlay.style.left = '0' } // First, type the summary await wait(DELAY_MEDIUM) const summaryInput = document.querySelector( 'input[placeholder="Summary"]' ) as HTMLInputElement if (summaryInput) { const summaryText = 'Validate temperature input' await typeText(summaryInput, summaryText) updateModuleSummary('a', summaryText) await wait(DELAY_LONG) } // Then, type the code let editorState = get(currentEditor) let attempts = 0 while (attempts < 20) { if (editorState && editorState.type === 'script' && editorState.stepId === 'a') { break } await wait(100) editorState = get(currentEditor) attempts++ } if (editorState && editorState.type === 'script') { const editor = editorState.editor const moduleA = flowJson.value.modules.find((m) => m.id === 'a') const codeToType = moduleA?.value && 'content' in moduleA.value ? moduleA.value.content : '' if (codeToType) { editor.setCode('', true) await wait(200) let currentText = '' for (let i = 0; i < codeToType.length; i++) { const char = codeToType[i] currentText += char editor.setCode(currentText, true) const delay = char === '\n' ? DELAY_CODE_NEWLINE : DELAY_CODE_CHAR await wait(delay) } // Update the flow store with the typed code const moduleIndex = flowStore.val.value.modules.findIndex((m) => m.id === 'a') if ( moduleIndex !== -1 && 'content' in flowStore.val.value.modules[moduleIndex].value ) { flowStore.val.value.modules[moduleIndex].value = { ...flowStore.val.value.modules[moduleIndex].value, content: codeToType } flowStore.val = { ...flowStore.val } } // Press Enter after finishing typing await wait(DELAY_MEDIUM) const model = editor.getModel() if (model && 'setValue' in model) { model.setValue(currentText + '\n') } // Mark step 4 as complete step4Complete = true } } }, popover: { title: 'Add validation logic', description: 'Watch as we write code to validate the temperature input.', side: 'bottom', onNextClick: () => { // Only proceed if code writing is complete if (!step4Complete) { sendUserToast( 'Please wait for the code to finish typing...', false, [], undefined, 3000 ) return } const driverOverlay = getDriverOverlay() if (driverOverlay) { driverOverlay.style.display = 'none' } const customOverlay = document.createElement('div') customOverlay.className = 'tutorial-custom-overlay' customOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 9999; pointer-events: none; clip-path: polygon( 0 0, 100% 0, 100% 50%, 50% 50%, 50% 100%, 0 100% ); ` document.body.appendChild(customOverlay) driver.moveNext() }, onPrevClick: () => { sendUserToast('Previous is not available for this step', true, [], undefined, 3000) } } }, { onHighlighted: async () => { step5Complete = false // Create a single cursor that will move continuously const fakeCursor = document.createElement('div') fakeCursor.style.cssText = ` position: fixed; width: 20px; height: 20px; border-radius: 50%; background-color: rgba(59, 130, 246, 0.8); border: 2px solid white; pointer-events: none; z-index: 10000; transition: all 1.5s ease-in-out; ` document.body.appendChild(fakeCursor) // Step 1: Move to and click plug button document.querySelector('#flow-editor-plug')?.parentElement?.classList.remove('opacity-0') await wait(DELAY_SHORT) const plugButton = document.querySelector('#flow-editor-plug') as HTMLElement if (plugButton) { const plugRect = plugButton.getBoundingClientRect() // Start from off-screen left fakeCursor.style.left = `${plugRect.left - 100}px` fakeCursor.style.top = `${plugRect.top + plugRect.height / 2}px` await wait(DELAY_SHORT) // Move to plug button fakeCursor.style.left = `${plugRect.left + plugRect.width / 2}px` fakeCursor.style.top = `${plugRect.top + plugRect.height / 2}px` await wait(DELAY_ANIMATION) await wait(DELAY_MEDIUM) clickButtonBySelector('#flow-editor-plug') } await wait(DELAY_MEDIUM) // Step 2: Move to and click flow_input.celsius const targetButton = document.querySelector( 'button[title="flow_input.celsius"]' ) as HTMLElement if (targetButton) { await moveCursorToElement(fakeCursor, targetButton, DELAY_ANIMATION_LONG) await wait(DELAY_MEDIUM) const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }) targetButton.dispatchEvent(clickEvent) } await wait(DELAY_LONG) // Step 3: Move to and click Test this step tab const testTabButton = findButtonByText('Test this step', ['border-b-2', 'cursor-pointer']) if (testTabButton) { await moveCursorToElement(fakeCursor, testTabButton, DELAY_ANIMATION) await wait(DELAY_SHORT) testTabButton.click() } await wait(DELAY_LONG) // Step 4: Move to and click Run button const testActionButton = findButtonByText('Run', ['bg-surface-accent-primary', 'w-full']) if (testActionButton) { await moveCursorToElement(fakeCursor, testActionButton, DELAY_ANIMATION) await wait(DELAY_MEDIUM) testActionButton.click() await wait(DELAY_MEDIUM) } // Remove cursor at the end fakeCursor.remove() step5Complete = true }, popover: { title: 'Wire it up and test', description: 'Connect the input, then run a quick test to verify the validation works.', onNextClick: async () => { if (!step5Complete) { sendUserToast('Please wait for the test to complete...', false, [], undefined, 3000) return } cleanupCustomOverlay() driver.moveNext() }, onPrevClick: () => { sendUserToast('Previous is not available for this step', true, [], undefined, 3000) } } }, { onHighlighted: async () => { step6Complete = false // First, add modules b and c with empty summaries const modulesToAdd = [flowJson.value.modules[1], flowJson.value.modules[2]] for (let i = 0; i < modulesToAdd.length; i++) { await new Promise((resolve) => setTimeout(resolve, i === 0 ? 0 : 700)) const moduleData = modulesToAdd[i] const module: FlowModule = { id: moduleData.id, summary: '', // Start with empty summary value: moduleData.value } await addModuleToFlow(module) } await wait(700) // Create a single cursor for continuous movement const fakeCursor = document.createElement('div') fakeCursor.style.cssText = ` position: fixed; width: 20px; height: 20px; border-radius: 50%; background-color: rgba(59, 130, 246, 0.8); border: 2px solid white; pointer-events: none; z-index: 10000; transition: all 1.5s ease-in-out; ` document.body.appendChild(fakeCursor) // Step 1: Click on script 'b' await wait(DELAY_MEDIUM) const scriptB = document.querySelector('#b') as HTMLElement if (scriptB) { const bRect = scriptB.getBoundingClientRect() // Start from off-screen fakeCursor.style.left = `${bRect.left - 100}px` fakeCursor.style.top = `${bRect.top + bRect.height / 2}px` await wait(DELAY_SHORT) // Move to script b fakeCursor.style.left = `${bRect.left + bRect.width / 2}px` fakeCursor.style.top = `${bRect.top + bRect.height / 2}px` await wait(DELAY_ANIMATION) await wait(DELAY_MEDIUM) selectionManager.selectId('b') } await wait(DELAY_LONG) // Type summary for script 'b' const summaryInputB = document.querySelector( 'input[placeholder="Summary"]' ) as HTMLInputElement if (summaryInputB) { const summaryTextB = 'Convert to Fahrenheit' await typeText(summaryInputB, summaryTextB) updateModuleSummary('b', summaryTextB) await wait(DELAY_LONG) } // Step 2: Move to and click on script 'c' const scriptC = document.querySelector('#c') as HTMLElement if (scriptC) { await moveCursorToElement(fakeCursor, scriptC, DELAY_ANIMATION) await wait(DELAY_SHORT) selectionManager.selectId('c') } await wait(DELAY_LONG) // Type summary for script 'c' const summaryInputC = document.querySelector( 'input[placeholder="Summary"]' ) as HTMLInputElement if (summaryInputC) { const summaryTextC = 'Categorize temperature' await typeText(summaryInputC, summaryTextC) updateModuleSummary('c', summaryTextC) await wait(DELAY_LONG) } // Move cursor to Test Flow button const testFlowButton = document.querySelector('#flow-editor-test-flow') as HTMLElement if (testFlowButton) { await moveCursorToElement(fakeCursor, testFlowButton, DELAY_ANIMATION) await wait(DELAY_MEDIUM) } // Remove cursor at the end fakeCursor.remove() step6Complete = true }, popover: { title: 'Add the final steps', description: 'Two more scripts to convert and categorize the temperature.', onNextClick: () => { if (!step6Complete) { sendUserToast( 'Please wait for the summaries to be added...', false, [], undefined, 3000 ) return } // Reset the driver.js overlay to full screen const driverOverlay = getDriverOverlay() if (driverOverlay) { driverOverlay.style.display = '' driverOverlay.style.width = '' driverOverlay.style.right = '' driverOverlay.style.left = '' } driver.moveNext() }, onPrevClick: () => { sendUserToast('Previous is not available for this step', true, [], undefined, 3000) } } }, { element: '#flow-editor-test-flow', popover: { title: 'Ready to test!', description: 'Run the complete flow and see your temperature converter in action.

💡 Want to learn more? Access more tutorials from the Tutorials page in the main menu or in the Help submenu.

', onNextClick: () => { updateProgress(index) driver.destroy() }, onPrevClick: () => { sendUserToast('Previous is not available for this step', true, [], undefined, 3000) } } } ] return steps }} />