import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
import { state } from '../state/index.js';
import { translate } from '../utils/i18nHelpers.js';
import { showToast } from '../utils/uiHelpers.js';
export class OnboardingManager {
constructor() {
this.isActive = false;
this.currentStep = 0;
this.selectedLanguage = 'en'; // Will be updated from state
this.overlay = null;
this.spotlight = null;
this.popup = null;
this.currentTarget = null; // Track current highlighted element
// Available languages with SVG flags (using flag-icons)
this.languages = [
{ code: 'en', name: 'English', flag: 'us' },
{ code: 'zh-CN', name: '简体中文', flag: 'cn' },
{ code: 'zh-TW', name: '繁體中文', flag: 'hk' },
{ code: 'ja', name: '日本語', flag: 'jp' },
{ code: 'ko', name: '한국어', flag: 'kr' },
{ code: 'es', name: 'Español', flag: 'es' },
{ code: 'fr', name: 'Français', flag: 'fr' },
{ code: 'de', name: 'Deutsch', flag: 'de' },
{ code: 'ru', name: 'Русский', flag: 'ru' }
];
// Tutorial steps configuration
this.steps = [
{
target: '.controls .action-buttons [data-action="fetch"]',
title: () => translate('onboarding.steps.fetch.title', {}, 'Fetch Models Metadata'),
content: () => translate('onboarding.steps.fetch.content', {}, 'Click the Fetch button to download model metadata and preview images from Civitai.'),
position: 'bottom'
},
{
target: '.controls .action-buttons [data-action="download"]',
title: () => translate('onboarding.steps.download.title', {}, 'Download New Models'),
content: () => translate('onboarding.steps.download.content', {}, 'Use the Download button to download models directly from Civitai URLs.'),
position: 'bottom'
},
{
target: '.controls .action-buttons [data-action="bulk"]',
title: () => translate('onboarding.steps.bulk.title', {}, 'Bulk Operations'),
content: () => translate('onboarding.steps.bulk.content', {}, 'Enter bulk mode by clicking this button or pressing B to select multiple models and perform batch operations.
• Ctrl/Cmd+A select all visible models, Shift+Click select a range.
• Esc or clicking an empty area exits bulk mode.'),
position: 'bottom'
},
{
target: '#searchOptionsToggle',
title: () => translate('onboarding.steps.searchOptions.title', {}, 'Search Options'),
content: () => translate('onboarding.steps.searchOptions.content', {}, 'Click this button to configure what fields to search in: filename, model name, tags, or creator name. Customize your search scope.'),
position: 'bottom'
},
{
target: '#filterButton',
title: () => translate('onboarding.steps.filter.title', {}, 'Filter Models'),
content: () => translate('onboarding.steps.filter.content', {}, 'Use filters to narrow down models by base model type (SD1.5, SDXL, Flux, etc.) or by specific tags.'),
position: 'bottom'
},
{
target: '#breadcrumbContainer',
title: () => translate('onboarding.steps.breadcrumb.title', {}, 'Breadcrumb Navigation'),
content: () => translate('onboarding.steps.breadcrumb.content', {}, 'The breadcrumb navigation shows your current path and allows quick navigation between folders. Click any folder name to jump directly there.'),
position: 'bottom'
},
{
target: '.card-grid',
title: () => translate('onboarding.steps.modelCards.title', {}, 'Model Cards'),
content: () => translate('onboarding.steps.modelCards.content', {}, 'Single-click a model card to view detailed information and edit metadata. Look for the pencil icon when hovering over editable fields.'),
position: 'top',
customPosition: { top: '20%', left: '50%' }
},
{
target: '.card-grid',
title: () => translate('onboarding.steps.marqueeSelect.title', {}, 'Drag to Select'),
content: () => translate('onboarding.steps.marqueeSelect.content', {}, 'Hold the left mouse button on an empty area of the grid and drag to draw a marquee that selects multiple cards at once.'),
position: 'top',
customPosition: { top: '20%', left: '50%' }
},
{
target: '#folderSidebar',
title: () => translate('onboarding.steps.dragToSidebar.title', {}, 'Organize by Dragging'),
content: () => translate('onboarding.steps.dragToSidebar.content', {}, 'Drag a model card onto a folder in the sidebar to move the file there. This also works with multiple selected cards in bulk mode.'),
position: 'right'
},
{
target: '.card-grid',
title: () => translate('onboarding.steps.contextMenu.title', {}, 'Context Menu'),
content: () => translate('onboarding.steps.contextMenu.content', {}, 'Right-click any model card for a context menu with card actions like moving, deleting, or editing metadata.'),
position: 'top',
customPosition: { top: '20%', left: '50%' }
},
{
target: '.card-grid',
title: () => translate('onboarding.steps.contextMenus.title', {}, 'More Context Menus'),
content: () => translate('onboarding.steps.contextMenus.content', {}, 'In bulk mode, right-click a selected card for bulk actions. Right-click an empty area of the page for global actions like update checks and managing excluded models.'),
position: 'top',
customPosition: { top: '20%', left: '50%' }
}
];
}
// Check if user should see onboarding
// First checks backend settings (persistent), falls back to localStorage
async shouldShowOnboarding() {
// Try to get state from backend first (persistent across browser modes)
try {
const response = await fetch('/api/lm/settings');
const data = await response.json();
if (data.success && data.settings && data.settings.onboarding_completed === true) {
// Sync to localStorage as cache
setStorageItem('onboarding_completed', true);
return false;
}
} catch (e) {
// Backend unavailable, fall back to localStorage
console.debug('Failed to fetch onboarding state from backend, using localStorage');
}
// Fallback to localStorage (for backward compatibility)
const completed = getStorageItem('onboarding_completed');
const skipped = getStorageItem('onboarding_skipped');
return !completed && !skipped;
}
// Start the onboarding process
async start() {
const shouldShow = await this.shouldShowOnboarding();
if (!shouldShow) {
return;
}
// If language has already been set, skip language selection
if (getStorageItem('onboarding_language_set')) {
this.startTutorial();
return;
}
// Show language selection first
await this.showLanguageSelection();
}
// Show language selection modal
showLanguageSelection() {
return new Promise((resolve) => {
// Initialize selected language from current settings
this.selectedLanguage = state.global.settings.language || 'en';
const modal = document.createElement('div');
modal.className = 'language-selection-modal';
modal.innerHTML = `
Choose Your Language / 选择语言 / 言語を選択
${typeof step.content === 'function' ? step.content() : step.content}