Files
BetterDesk/web-nodejs/public/js/tutorial.js
T
UNITRONIX 4eed88673c fix(updates): atomic binary replace + accurate modal status
- deployServerBinary: use rename(2) for atomic replace, fixes ETXTBSY
  when target Go binary is busy (Linux kernel handles inode swap).
  Falls back to copyFileSync on cross-device rename or non-Linux.
  Windows: rename target out of the way first, then move new in.
- settings.js: mark 'server' phase as error when build succeeded but
  deploy failed (was incorrectly marking 'done' from build alone).
- settings.js: completion modal now shows error title, error message
  and pre-formatted stderr when serverDeploy.success === false.
- i18n: added complete_with_errors, modal_done_with_errors_title in
  en/pl.
2026-04-26 01:23:18 +02:00

873 lines
35 KiB
JavaScript

/**
* BetterDesk Console - Visual Tutorial System
* Spotlight-based guided tour with i18n support.
*
* Usage:
* Tutorial.start('console'); // Console panel tutorial
* Tutorial.start('desktop'); // Desktop mode tutorial
* Tutorial.start('devices'); // Devices page tutorial
* Tutorial.start('settings'); // Settings page tutorial
* Tutorial.skip(); // Close tutorial
* Tutorial.autoStart('console'); // Auto-start on first visit
*/
(function() {
'use strict';
// ============ Constants ============
var STORAGE_SEEN = 'betterdesk_tutorial_seen';
var STORAGE_DISABLED = 'betterdesk_tutorial_disabled';
// ============ State ============
var _overlay = null;
var _spotlight = null;
var _tooltip = null;
var _steps = [];
var _currentStep = 0;
var _isActive = false;
var _onComplete = null;
var _tutorialType = 'console';
var _resizeHandler = null;
// ============ i18n Helper ============
function t(key, fallback) {
if (typeof window._ === 'function') {
var result = window._(key);
return result !== key ? result : fallback;
}
if (window.BetterDesk && window.BetterDesk.translations) {
var keys = key.split('.');
var val = window.BetterDesk.translations;
for (var i = 0; i < keys.length; i++) {
if (val && typeof val === 'object' && keys[i] in val) {
val = val[keys[i]];
} else {
return fallback;
}
}
return val || fallback;
}
return fallback;
}
// ============ Tutorial Definitions ============
function getConsoleTutorialSteps() {
return [
{
selector: '.sidebar-rail',
title: t('tutorial.console_sidebar_title', 'Navigation Sidebar'),
text: t('tutorial.console_sidebar_text', 'Access all modules from here. Click any icon to navigate to that section. The sidebar adapts to your screen size.'),
position: 'right',
highlight: true
},
{
selector: '.sidebar-rail-bottom .sidebar-rail-btn[title]',
title: t('tutorial.console_expand_title', 'Expand Sidebar'),
text: t('tutorial.console_expand_text', 'Click here to expand the sidebar and see full labels for all navigation items. Click again to collapse.'),
position: 'right',
highlight: true
},
{
selector: '.navbar .search-box',
title: t('tutorial.console_search_title', 'Quick Search'),
text: t('tutorial.console_search_text', 'Search for devices, settings, or actions quickly. Press Ctrl+K for keyboard shortcut.'),
position: 'bottom',
highlight: true
},
{
selector: '.content-wrapper',
title: t('tutorial.console_dashboard_title', 'Dashboard'),
text: t('tutorial.console_dashboard_text', 'Your central hub showing server status, online devices, and quick actions. Keep an eye on important metrics at a glance.'),
position: 'center',
highlight: false
},
{
selector: '#desktop-toggle-btn',
title: t('tutorial.console_desktop_title', 'Desktop Mode'),
text: t('tutorial.console_desktop_text', 'Switch to desktop mode for a windowed workspace with widgets. Perfect for multi-tasking and monitoring dashboards.'),
position: 'left',
highlight: true
},
{
selector: null,
title: t('tutorial.console_complete_title', 'You\'re Ready!'),
text: t('tutorial.console_complete_text', 'Explore the sidebar categories to discover all features. Click the Help icon anytime to restart this guide.'),
position: 'center',
highlight: false,
final: true
}
];
}
function getDesktopTutorialSteps() {
return [
{
selector: '.desktop-topbar',
title: t('tutorial.desktop_topbar_title', 'Top Bar'),
text: t('tutorial.desktop_topbar_text', 'The top bar shows the clock, quick controls, and access to the app drawer. Click the grid icon to browse all apps.'),
position: 'bottom',
highlight: true
},
{
selector: '.app-drawer-overlay .app-drawer',
title: t('tutorial.desktop_drawer_title', 'App Drawer'),
text: t('tutorial.desktop_drawer_text', 'Open the app drawer to search and launch any console function as a floating window. Apps are organized by category.'),
position: 'bottom',
highlight: true,
beforeShow: function() {
var btn = document.querySelector('.topbar-app-drawer-btn');
if (btn) btn.click();
},
afterHide: function() {
var overlay = document.querySelector('.app-drawer-overlay');
if (overlay && overlay.classList.contains('open')) {
overlay.classList.remove('open');
}
}
},
{
selector: '.widget-canvas',
title: t('tutorial.desktop_widgets_title', 'Widget Dashboard'),
text: t('tutorial.desktop_widgets_text', 'Your customizable workspace. Widgets display live information and provide quick access to features. Drag to rearrange.'),
position: 'center',
highlight: false
},
{
selector: '.desktop-taskbar',
title: t('tutorial.desktop_taskbar_title', 'Taskbar'),
text: t('tutorial.desktop_taskbar_text', 'Open apps appear here. Click to focus or minimize windows. The clock shows current time and wallpaper button changes the background.'),
position: 'top',
highlight: true
},
{
selector: null,
title: t('tutorial.desktop_snap_title', 'Snap Layouts'),
text: t('tutorial.desktop_snap_text', 'Drag windows to screen edges to snap them into position, or hover the maximize button for layout presets. Try Aero Shake!'),
position: 'center',
highlight: false
},
{
selector: null,
title: t('tutorial.desktop_complete_title', 'Desktop Mode Ready!'),
text: t('tutorial.desktop_complete_text', 'Try opening apps from the drawer and arranging windows side-by-side. Use widget presets for quick dashboard layouts.'),
position: 'center',
highlight: false,
final: true
}
];
}
function getDevicesTutorialSteps() {
return [
{
selector: '.folder-chips',
title: t('tutorial.devices_folders_title', 'Device Folders'),
text: t('tutorial.devices_folders_text', 'Organize devices into folders. Click a folder to filter the list. Right-click to rename or delete.'),
position: 'bottom',
highlight: true
},
{
selector: '.search-devices',
title: t('tutorial.devices_search_title', 'Search & Filter'),
text: t('tutorial.devices_search_text', 'Search by ID, hostname, or platform. Use the status pills to show only online, offline, or all devices.'),
position: 'bottom',
highlight: true
},
{
selector: '.devices-table',
title: t('tutorial.devices_table_title', 'Device List'),
text: t('tutorial.devices_table_text', 'Click any device row for details. Use the kebab menu on the right for quick actions like connect, edit tags, or delete.'),
position: 'center',
highlight: false
},
{
selector: null,
title: t('tutorial.devices_complete_title', 'Device Management Ready!'),
text: t('tutorial.devices_complete_text', 'Drag devices between folders to organize them. Double-click a device to open its detail panel.'),
position: 'center',
highlight: false,
final: true
}
];
}
function getSettingsTutorialSteps() {
return [
{
selector: '.settings-tabs, .settings-nav',
title: t('tutorial.settings_tabs_title', 'Settings Categories'),
text: t('tutorial.settings_tabs_text', 'Navigate between General, Security, Appearance, and other configuration sections.'),
position: 'right',
highlight: true
},
{
selector: '.settings-content, .settings-panel',
title: t('tutorial.settings_content_title', 'Configuration'),
text: t('tutorial.settings_content_text', 'Each section contains specific server and console settings. Changes are saved when you click Save.'),
position: 'center',
highlight: false
},
{
selector: null,
title: t('tutorial.settings_complete_title', 'Settings Overview'),
text: t('tutorial.settings_complete_text', 'Check Security settings for 2FA, API keys and user management. Visit Appearance to customize the console look.'),
position: 'center',
highlight: false,
final: true
}
];
}
function getStepsForType(type) {
switch (type) {
case 'desktop': return getDesktopTutorialSteps();
case 'devices': return getDevicesTutorialSteps();
case 'settings': return getSettingsTutorialSteps();
case 'remote': return getRemoteTutorialSteps();
case 'organization': return getOrganizationTutorialSteps();
case 'cdap': return getCDAPTutorialSteps();
case 'chat': return getChatTutorialSteps();
default: return getConsoleTutorialSteps();
}
}
function getRemoteTutorialSteps() {
return [
{
selector: '.remote-toolbar, .toolbar',
title: t('tutorial.remote_toolbar_title', 'Remote Toolbar'),
text: t('tutorial.remote_toolbar_text', 'Access display controls, clipboard sync, special keys, and quality settings from the toolbar. Toggle fullscreen with F11.'),
position: 'bottom',
highlight: true
},
{
selector: '#remote-canvas, .remote-canvas',
title: t('tutorial.remote_canvas_title', 'Remote Display'),
text: t('tutorial.remote_canvas_text', 'This is the remote screen. Click inside to start controlling the remote device. Mouse and keyboard input are forwarded automatically.'),
position: 'center',
highlight: false
},
{
selector: '.clipboard-btn, .toolbar-clipboard',
title: t('tutorial.remote_clipboard_title', 'Clipboard Sync'),
text: t('tutorial.remote_clipboard_text', 'Copy and paste text between your computer and the remote device. Use Ctrl+C / Ctrl+V as normal while focused on the remote display.'),
position: 'bottom',
highlight: true
},
{
selector: null,
title: t('tutorial.remote_complete_title', 'Remote Session Ready!'),
text: t('tutorial.remote_complete_text', 'Use the toolbar buttons for special keys (Ctrl+Alt+Del, PrintScreen) and monitor switching. Press Escape or click Disconnect to end the session.'),
position: 'center',
highlight: false,
final: true
}
];
}
function getOrganizationTutorialSteps() {
return [
{
selector: '.org-header, .organizations-page h1',
title: t('tutorial.org_overview_title', 'Organizations'),
text: t('tutorial.org_overview_text', 'Organizations let you group users, devices, and policies. Each organization has its own admin, operators, and settings.'),
position: 'bottom',
highlight: true
},
{
selector: '.org-create-btn, .btn-create-org',
title: t('tutorial.org_create_title', 'Create Organization'),
text: t('tutorial.org_create_text', 'Click here to create a new organization. Provide a name, slug, and optional description. You will be the owner of the new organization.'),
position: 'bottom',
highlight: true
},
{
selector: '.org-members, .org-users-tab',
title: t('tutorial.org_members_title', 'Manage Members'),
text: t('tutorial.org_members_text', 'Invite users by email, assign roles (admin, operator, user), and manage access. Members can see devices assigned to their organization.'),
position: 'right',
highlight: true
},
{
selector: '.org-devices, .org-devices-tab',
title: t('tutorial.org_devices_title', 'Assign Devices'),
text: t('tutorial.org_devices_text', 'Add devices to the organization. Devices assigned here will inherit the organization\'s policies and be visible to its members.'),
position: 'right',
highlight: true
},
{
selector: '.org-policies, .org-settings-tab',
title: t('tutorial.org_policies_title', 'Organization Policies'),
text: t('tutorial.org_policies_text', 'Set security policies like password requirements, session timeouts, 2FA enforcement, and device enrollment rules for the organization.'),
position: 'right',
highlight: true
},
{
selector: null,
title: t('tutorial.org_complete_title', 'Organization Setup Complete!'),
text: t('tutorial.org_complete_text', 'Invite your team and assign devices to start using organization-scoped management. Policies apply automatically to all members.'),
position: 'center',
highlight: false,
final: true
}
];
}
function getCDAPTutorialSteps() {
return [
{
selector: '.cdap-device-list, .cdap-devices',
title: t('tutorial.cdap_devices_title', 'CDAP Devices'),
text: t('tutorial.cdap_devices_text', 'View all connected CDAP devices — IoT sensors, PLCs, bridges, and custom agents. Green indicators show live connections.'),
position: 'bottom',
highlight: true
},
{
selector: '.cdap-widget-grid, .cdap-widgets',
title: t('tutorial.cdap_widgets_title', 'Widget Dashboard'),
text: t('tutorial.cdap_widgets_text', 'Widgets display real-time data from devices. Gauges, toggles, LEDs, charts — each widget type visualizes a different kind of data.'),
position: 'center',
highlight: false
},
{
selector: '.cdap-command-panel, .cdap-commands',
title: t('tutorial.cdap_commands_title', 'Send Commands'),
text: t('tutorial.cdap_commands_text', 'Interact with devices by sending commands. Some commands need confirmation. The command log keeps a history of all sent commands.'),
position: 'left',
highlight: true
},
{
selector: '.cdap-terminal-btn, .cdap-terminal',
title: t('tutorial.cdap_terminal_title', 'Device Terminal'),
text: t('tutorial.cdap_terminal_text', 'Open a terminal session to CDAP agents for advanced diagnostics or manual configuration. Type commands directly.'),
position: 'bottom',
highlight: true
},
{
selector: null,
title: t('tutorial.cdap_complete_title', 'CDAP Overview Complete!'),
text: t('tutorial.cdap_complete_text', 'Explore device widgets, send commands, and use the terminal or file browser for deeper management. CDAP agents report metrics automatically.'),
position: 'center',
highlight: false,
final: true
}
];
}
function getChatTutorialSteps() {
return [
{
selector: '.chat-contacts, .chat-sidebar',
title: t('tutorial.chat_contacts_title', 'Contacts & Groups'),
text: t('tutorial.chat_contacts_text', 'See your contacts and chat groups. Click any contact to open a conversation. Green dots indicate online users.'),
position: 'right',
highlight: true
},
{
selector: '.chat-messages, .chat-area',
title: t('tutorial.chat_send_title', 'Send Messages'),
text: t('tutorial.chat_send_text', 'Type your message and press Enter to send. Messages are end-to-end encrypted when both parties support E2E.'),
position: 'center',
highlight: false
},
{
selector: '.chat-file-btn, .chat-attach',
title: t('tutorial.chat_file_title', 'Share Files'),
text: t('tutorial.chat_file_text', 'Click the attachment icon to share files. Files are encrypted and can be up to 50 MB.'),
position: 'top',
highlight: true
},
{
selector: null,
title: t('tutorial.chat_complete_title', 'Chat Ready!'),
text: t('tutorial.chat_complete_text', 'Chat with operators and end users in real time. Create groups for team communication. All messages support read receipts.'),
position: 'center',
highlight: false,
final: true
}
];
}
// ============ DOM Creation ============
function createOverlay() {
if (_overlay) return;
_overlay = document.createElement('div');
_overlay.className = 'tutorial-overlay';
_overlay.id = 'tutorial-overlay';
_spotlight = document.createElement('div');
_spotlight.className = 'tutorial-spotlight';
_overlay.appendChild(_spotlight);
_tooltip = document.createElement('div');
_tooltip.className = 'tutorial-tooltip';
_tooltip.innerHTML =
'<div class="tutorial-tooltip-header">' +
'<span class="tutorial-title"></span>' +
'<button class="tutorial-close" title="' + t('tutorial.close', 'Close') + '">' +
'<span class="material-icons">close</span>' +
'</button>' +
'</div>' +
'<div class="tutorial-tooltip-body"></div>' +
'<div class="tutorial-tooltip-footer">' +
'<span class="tutorial-progress"></span>' +
'<div class="tutorial-actions">' +
'<button class="tutorial-btn tutorial-skip">' + t('tutorial.skip', 'Skip') + '</button>' +
'<button class="tutorial-btn tutorial-prev">' + t('tutorial.prev', 'Previous') + '</button>' +
'<button class="tutorial-btn tutorial-btn-primary tutorial-next">' + t('tutorial.next', 'Next') + '</button>' +
'</div>' +
'</div>';
_overlay.appendChild(_tooltip);
document.body.appendChild(_overlay);
_tooltip.querySelector('.tutorial-close').addEventListener('click', skip);
_tooltip.querySelector('.tutorial-skip').addEventListener('click', skip);
_tooltip.querySelector('.tutorial-prev').addEventListener('click', prevStep);
_tooltip.querySelector('.tutorial-next').addEventListener('click', nextStep);
_overlay.addEventListener('click', function(e) {
if (e.target === _overlay) skip();
});
document.addEventListener('keydown', handleKeyDown);
_resizeHandler = function() {
if (_isActive && _steps[_currentStep]) {
positionElements(_steps[_currentStep]);
}
};
window.addEventListener('resize', _resizeHandler);
}
function removeOverlay() {
document.removeEventListener('keydown', handleKeyDown);
if (_resizeHandler) {
window.removeEventListener('resize', _resizeHandler);
_resizeHandler = null;
}
if (_overlay) {
_overlay.remove();
_overlay = null;
_spotlight = null;
_tooltip = null;
}
}
// ============ Step Navigation ============
function showStep(index) {
if (!_steps.length || index < 0 || index >= _steps.length) return;
// Run afterHide for current step
var prevStepObj = _steps[_currentStep];
if (prevStepObj && prevStepObj.afterHide && index !== _currentStep) {
prevStepObj.afterHide();
}
_currentStep = index;
var step = _steps[index];
// Run beforeShow for new step
if (step.beforeShow) {
step.beforeShow();
// Small delay to let DOM update
setTimeout(function() { renderStep(step, index); }, 200);
} else {
renderStep(step, index);
}
}
function renderStep(step, index) {
// Transition animation
_tooltip.classList.add('step-transition');
setTimeout(function() { _tooltip.classList.remove('step-transition'); }, 250);
_tooltip.querySelector('.tutorial-title').textContent = step.title || '';
_tooltip.querySelector('.tutorial-tooltip-body').textContent = step.text || '';
_tooltip.querySelector('.tutorial-progress').innerHTML =
'<div class="tutorial-progress-dots">' +
_steps.map(function(_, i) {
var cls = i === index ? 'active' : (i < index ? 'seen' : '');
return '<span class="tutorial-progress-dot ' + cls + '"></span>';
}).join('') +
'</div>';
var prevBtn = _tooltip.querySelector('.tutorial-prev');
var nextBtn = _tooltip.querySelector('.tutorial-next');
var skipBtn = _tooltip.querySelector('.tutorial-skip');
prevBtn.style.display = index > 0 ? '' : 'none';
nextBtn.textContent = step.final ? t('tutorial.finish', 'Finish') : t('tutorial.next', 'Next');
skipBtn.style.display = step.final ? 'none' : '';
positionElements(step);
}
function positionElements(step) {
var target = step.selector ? document.querySelector(step.selector) : null;
if (target && step.highlight) {
var rect = target.getBoundingClientRect();
var padding = 8;
_spotlight.style.display = 'block';
_spotlight.style.left = (rect.left - padding) + 'px';
_spotlight.style.top = (rect.top - padding) + 'px';
_spotlight.style.width = (rect.width + padding * 2) + 'px';
_spotlight.style.height = (rect.height + padding * 2) + 'px';
positionTooltip(rect, step.position);
} else {
_spotlight.style.display = 'none';
_tooltip.style.left = '50%';
_tooltip.style.top = '50%';
_tooltip.style.transform = 'translate(-50%, -50%)';
}
}
function positionTooltip(targetRect, position) {
var tooltipRect = _tooltip.getBoundingClientRect();
var gap = 16;
var left, top;
switch (position) {
case 'top':
left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
top = targetRect.top - tooltipRect.height - gap;
break;
case 'bottom':
left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
top = targetRect.bottom + gap;
break;
case 'left':
left = targetRect.left - tooltipRect.width - gap;
top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2;
break;
case 'right':
left = targetRect.right + gap;
top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2;
break;
default:
left = window.innerWidth / 2 - tooltipRect.width / 2;
top = window.innerHeight / 2 - tooltipRect.height / 2;
}
left = Math.max(16, Math.min(left, window.innerWidth - tooltipRect.width - 16));
top = Math.max(16, Math.min(top, window.innerHeight - tooltipRect.height - 16));
_tooltip.style.left = left + 'px';
_tooltip.style.top = top + 'px';
_tooltip.style.transform = 'none';
}
function nextStep() {
if (_currentStep < _steps.length - 1) {
showStep(_currentStep + 1);
} else {
complete();
}
}
function prevStep() {
if (_currentStep > 0) {
showStep(_currentStep - 1);
}
}
function handleKeyDown(e) {
if (!_isActive) return;
if (e.key === 'Escape') {
skip();
} else if (e.key === 'ArrowRight' || e.key === 'Enter') {
nextStep();
} else if (e.key === 'ArrowLeft') {
prevStep();
}
}
// ============ Lifecycle ============
function start(type, callback) {
if (_isActive) return;
_tutorialType = type || 'console';
_steps = getStepsForType(_tutorialType);
// Filter steps whose target elements don't exist (skip gracefully)
_steps = _steps.filter(function(step) {
if (!step.selector) return true;
return document.querySelector(step.selector) !== null;
});
if (!_steps.length) return;
_isActive = true;
_currentStep = 0;
_onComplete = callback;
createOverlay();
requestAnimationFrame(function() {
_overlay.classList.add('active');
showStep(0);
});
}
function skip() {
if (!_isActive) return;
// Permanently mark this tutorial as dismissed so autoStart won't
// show it again until the user explicitly resets via the Help menu.
var seen = JSON.parse(localStorage.getItem(STORAGE_SEEN) || '{}');
seen[_tutorialType] = true;
localStorage.setItem(STORAGE_SEEN, JSON.stringify(seen));
// Run afterHide for current step
var step = _steps[_currentStep];
if (step && step.afterHide) step.afterHide();
_isActive = false;
_overlay.classList.remove('active');
setTimeout(function() {
removeOverlay();
_steps = [];
_currentStep = 0;
}, 300);
}
function complete() {
var seen = JSON.parse(localStorage.getItem(STORAGE_SEEN) || '{}');
seen[_tutorialType] = true;
localStorage.setItem(STORAGE_SEEN, JSON.stringify(seen));
// Run afterHide for current step
var step = _steps[_currentStep];
if (step && step.afterHide) step.afterHide();
_isActive = false;
_overlay.classList.remove('active');
setTimeout(function() {
removeOverlay();
if (typeof _onComplete === 'function') _onComplete();
_steps = [];
_currentStep = 0;
_onComplete = null;
}, 300);
}
function hasSeenTutorial(type) {
var seen = JSON.parse(localStorage.getItem(STORAGE_SEEN) || '{}');
return seen[type || 'console'] === true;
}
function resetTutorial(type) {
var seen = JSON.parse(localStorage.getItem(STORAGE_SEEN) || '{}');
if (type) {
delete seen[type];
} else {
seen = {};
}
localStorage.setItem(STORAGE_SEEN, JSON.stringify(seen));
}
function setDisabled(disabled) {
localStorage.setItem(STORAGE_DISABLED, disabled ? 'true' : 'false');
}
function isDisabled() {
return localStorage.getItem(STORAGE_DISABLED) === 'true';
}
function autoStart(type) {
if (isDisabled()) return;
if (hasSeenTutorial(type)) return;
setTimeout(function() {
start(type);
}, 1500);
}
// ============ Export ============
function toggleHelpMenu() {
if (!_helpMenu) showHelpButton();
if (_helpMenuOpen) closeHelpMenu(); else openHelpMenu();
}
window.Tutorial = {
start: start,
skip: skip,
hasSeenTutorial: hasSeenTutorial,
resetTutorial: resetTutorial,
setDisabled: setDisabled,
isDisabled: isDisabled,
autoStart: autoStart,
showHelpButton: showHelpButton,
hideHelpButton: hideHelpButton,
toggleHelpMenu: toggleHelpMenu
};
// Backward compatibility alias
window.BetterDeskTutorial = window.Tutorial;
// ============ Floating Help Button ============
var _helpBtn = null;
var _helpMenu = null;
var _helpMenuOpen = false;
function showHelpButton() {
if (_helpBtn) { _helpBtn.classList.add('visible'); return; }
_helpBtn = document.createElement('button');
_helpBtn.className = 'tutorial-help-btn';
_helpBtn.title = t('tutorial.help', 'Help & Tutorials');
_helpBtn.innerHTML = '<span class="material-icons">help_outline</span>';
document.body.appendChild(_helpBtn);
_helpMenu = document.createElement('div');
_helpMenu.className = 'tutorial-help-menu';
var tutorials = [
{ type: 'console', icon: 'dashboard', label: t('tutorial.tour_console', 'Console Tour') },
{ type: 'desktop', icon: 'desktop_windows', label: t('tutorial.tour_desktop', 'Desktop Mode Tour') },
{ type: 'devices', icon: 'devices', label: t('tutorial.tour_devices', 'Devices Tour') },
{ type: 'remote', icon: 'connected_tv', label: t('tutorial.tour_remote', 'Remote Session Tour') },
{ type: 'organization', icon: 'corporate_fare', label: t('tutorial.tour_organization', 'Organization Setup') },
{ type: 'cdap', icon: 'sensors', label: t('tutorial.tour_cdap', 'CDAP Overview') },
{ type: 'chat', icon: 'chat', label: t('tutorial.tour_chat', 'Chat Basics') },
{ type: 'settings', icon: 'settings', label: t('tutorial.tour_settings', 'Settings Tour') }
];
tutorials.forEach(function(tut) {
var btn = document.createElement('button');
btn.className = 'tutorial-help-menu-item';
btn.innerHTML = '<span class="material-icons">' + tut.icon + '</span>' + esc(tut.label);
btn.addEventListener('click', function() {
closeHelpMenu();
start(tut.type);
});
_helpMenu.appendChild(btn);
});
// Divider + reset option
var div = document.createElement('div');
div.className = 'tutorial-help-menu-divider';
_helpMenu.appendChild(div);
var resetBtn = document.createElement('button');
resetBtn.className = 'tutorial-help-menu-item';
resetBtn.innerHTML = '<span class="material-icons">refresh</span>' + esc(t('tutorial.reset_all', 'Reset All Tutorials'));
resetBtn.addEventListener('click', function() {
closeHelpMenu();
resetTutorial();
});
_helpMenu.appendChild(resetBtn);
// Disable/Enable toggle
var toggleDiv = document.createElement('div');
toggleDiv.className = 'tutorial-help-menu-divider';
_helpMenu.appendChild(toggleDiv);
var disableBtn = document.createElement('button');
disableBtn.className = 'tutorial-help-menu-item tutorial-toggle-btn';
_updateToggleBtn(disableBtn);
disableBtn.addEventListener('click', function() {
var nowDisabled = !isDisabled();
setDisabled(nowDisabled);
_updateToggleBtn(disableBtn);
closeHelpMenu();
if (nowDisabled) {
hideHelpButton();
}
// Notify settings page if open
window.dispatchEvent(new CustomEvent('tutorial:stateChanged', { detail: { disabled: nowDisabled } }));
});
_helpMenu.appendChild(disableBtn);
document.body.appendChild(_helpMenu);
_helpBtn.addEventListener('click', function(e) {
e.stopPropagation();
_helpMenuOpen ? closeHelpMenu() : openHelpMenu();
});
document.addEventListener('click', function() {
if (_helpMenuOpen) closeHelpMenu();
});
requestAnimationFrame(function() {
_helpBtn.classList.add('visible');
});
}
function hideHelpButton() {
if (_helpBtn) _helpBtn.classList.remove('visible');
closeHelpMenu();
}
function openHelpMenu() {
if (!_helpMenu) return;
_helpMenuOpen = true;
_helpMenu.classList.add('open');
}
function closeHelpMenu() {
if (!_helpMenu) return;
_helpMenuOpen = false;
_helpMenu.classList.remove('open');
}
function esc(s) {
var el = document.createElement('span');
el.textContent = s || '';
return el.innerHTML;
}
function _updateToggleBtn(btn) {
if (!btn) return;
if (isDisabled()) {
btn.innerHTML = '<span class="material-icons">visibility</span>' + esc(t('tutorial.enable_tutorials', 'Enable Tutorials'));
} else {
btn.innerHTML = '<span class="material-icons">visibility_off</span>' + esc(t('tutorial.disable_tutorials', 'Disable Tutorials'));
}
}
// Listen for settings page toggle
window.addEventListener('tutorial:stateChanged', function(e) {
if (e.detail && typeof e.detail.disabled === 'boolean') {
setDisabled(e.detail.disabled);
if (e.detail.disabled) {
hideHelpButton();
} else {
showHelpButton();
}
// Update toggle button text if menu exists
if (_helpMenu) {
var tb = _helpMenu.querySelector('.tutorial-toggle-btn');
_updateToggleBtn(tb);
}
}
});
// Auto-show help button after DOM ready (only if not disabled)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
if (!isDisabled()) showHelpButton();
});
} else {
if (!isDisabled()) showHelpButton();
}
})();