diff --git a/py/services/model_scanner.py b/py/services/model_scanner.py index e975d0b4..91834f1f 100644 --- a/py/services/model_scanner.py +++ b/py/services/model_scanner.py @@ -62,11 +62,16 @@ class ModelScanner: # Set initializing flag to true self._is_initializing = True + # Determine the page type based on model type + page_type = 'loras' if self.model_type == 'lora' else 'checkpoints' + # First, count all model files to track progress await ws_manager.broadcast_init_progress({ 'stage': 'scan_folders', 'progress': 0, - 'details': f"Scanning {self.model_type} folders..." + 'details': f"Scanning {self.model_type} folders...", + 'scanner_type': self.model_type, + 'pageType': page_type }) # Count files in a separate thread to avoid blocking @@ -79,7 +84,9 @@ class ModelScanner: await ws_manager.broadcast_init_progress({ 'stage': 'count_models', 'progress': 1, # Changed from 10 to 1 - 'details': f"Found {total_files} {self.model_type} files" + 'details': f"Found {total_files} {self.model_type} files", + 'scanner_type': self.model_type, + 'pageType': page_type }) start_time = time.time() @@ -88,14 +95,17 @@ class ModelScanner: await loop.run_in_executor( None, # Use default thread pool self._initialize_cache_sync, # Run synchronous version in thread - total_files # Pass the total file count for progress reporting + total_files, # Pass the total file count for progress reporting + page_type # Pass the page type for progress reporting ) # Send final progress update await ws_manager.broadcast_init_progress({ 'stage': 'finalizing', 'progress': 99, # Changed from 95 to 99 - 'details': f"Finalizing {self.model_type} cache..." + 'details': f"Finalizing {self.model_type} cache...", + 'scanner_type': self.model_type, + 'pageType': page_type }) logger.info(f"{self.model_type.capitalize()} cache initialized in {time.time() - start_time:.2f} seconds. Found {len(self._cache.raw_data)} models") @@ -106,7 +116,9 @@ class ModelScanner: 'stage': 'finalizing', 'progress': 100, 'status': 'complete', - 'details': f"Completed! Found {len(self._cache.raw_data)} {self.model_type} files." + 'details': f"Completed! Found {len(self._cache.raw_data)} {self.model_type} files.", + 'scanner_type': self.model_type, + 'pageType': page_type }) except Exception as e: @@ -154,7 +166,7 @@ class ModelScanner: return total_files - def _initialize_cache_sync(self, total_files=0): + def _initialize_cache_sync(self, total_files=0, page_type='loras'): """Synchronous version of cache initialization for thread pool execution""" try: # Create a new event loop for this thread @@ -222,7 +234,9 @@ class ModelScanner: await ws_manager.broadcast_init_progress({ 'stage': 'process_models', 'progress': progress_percent, - 'details': f"Processing {self.model_type} files: {processed_files}/{total_files}" + 'details': f"Processing {self.model_type} files: {processed_files}/{total_files}", + 'scanner_type': self.model_type, + 'pageType': page_type }) elif entry.is_dir(follow_symlinks=True): @@ -299,6 +313,9 @@ class ModelScanner: # Clear existing tags count self._tags_count = {} + # Determine the page type based on model type + page_type = 'loras' if self.model_type == 'lora' else 'checkpoints' + # Scan for new data raw_data = await self.scan_all_models() diff --git a/py/services/websocket_manager.py b/py/services/websocket_manager.py index 7f41d85b..8e35f601 100644 --- a/py/services/websocket_manager.py +++ b/py/services/websocket_manager.py @@ -55,6 +55,14 @@ class WebSocketManager: if not self._init_websockets: return + # Ensure data has all required fields + if 'stage' not in data: + data['stage'] = 'processing' + if 'progress' not in data: + data['progress'] = 0 + if 'details' not in data: + data['details'] = 'Processing...' + for ws in self._init_websockets: try: await ws.send_json(data) diff --git a/static/css/components/initialization.css b/static/css/components/initialization.css index a6a98eca..786ebd8d 100644 --- a/static/css/components/initialization.css +++ b/static/css/components/initialization.css @@ -2,16 +2,28 @@ .initialization-container { width: 100%; + height: 100%; padding: var(--space-3); background: var(--lora-surface); - border-radius: var(--border-radius-base); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); animation: fadeIn 0.3s ease-in-out; + display: flex; + align-items: center; + justify-content: center; } .initialization-content { max-width: 800px; - margin: 0 auto; + width: 100%; +} + +/* Override loading.css width for initialization component */ +.initialization-container .loading-content { + width: 100%; + max-width: 100%; + background: transparent; + backdrop-filter: none; + border: none; + padding: 0; } .initialization-header { @@ -53,11 +65,20 @@ width: 0%; } +/* Progress Details */ .progress-details { display: flex; justify-content: space-between; font-size: 0.9rem; color: var(--text-color); + margin-top: var(--space-1); + padding: 0 2px; +} + +#remainingTime { + font-style: italic; + color: var(--text-color); + opacity: 0.8; } /* Stages Styles */ @@ -143,49 +164,92 @@ color: rgb(0, 150, 0); } -/* Tips Styles */ -.initialization-tips { +/* Tips Container */ +.tips-container { + margin-top: var(--space-3); background: rgba(var(--lora-accent), 0.05); border-radius: var(--border-radius-base); padding: var(--space-2); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .tips-header { display: flex; align-items: center; - margin-bottom: var(--space-1); - color: var(--text-color); + margin-bottom: var(--space-2); + padding-bottom: var(--space-1); + border-bottom: 1px solid var(--lora-border); } .tips-header i { - margin-right: 8px; + margin-right: 10px; color: var(--lora-accent); + font-size: 1.2rem; } .tips-header h3 { - font-size: 1.1rem; + font-size: 1.2rem; margin: 0; + color: var(--text-color); +} + +/* Tip Carousel with Images */ +.tips-content { + position: relative; } .tip-carousel { position: relative; - height: 60px; + height: 160px; overflow: hidden; } .tip-item { position: absolute; width: 100%; + height: 100%; + display: flex; opacity: 0; transition: opacity 0.5s ease; - padding: 0 var(--space-1); + padding: 0; + border-radius: var(--border-radius-sm); + overflow: hidden; } .tip-item.active { opacity: 1; } -.tip-item p { +.tip-image { + width: 40%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--lora-border); +} + +.tip-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.tip-text { + width: 60%; + padding: var(--space-2); + display: flex; + flex-direction: column; + justify-content: center; +} + +.tip-text h4 { + margin: 0 0 var(--space-1) 0; + font-size: 1.1rem; + color: var(--text-color); +} + +.tip-text p { margin: 0; line-height: 1.5; font-size: 0.9rem; @@ -195,17 +259,21 @@ .tip-navigation { display: flex; justify-content: center; - margin-top: var(--space-1); + margin-top: var(--space-2); } .tip-dot { - width: 8px; - height: 8px; + width: 10px; + height: 10px; border-radius: 50%; background-color: var(--lora-border); - margin: 0 4px; + margin: 0 5px; cursor: pointer; - transition: background-color 0.2s ease; + transition: background-color 0.2s ease, transform 0.2s ease; +} + +.tip-dot:hover { + transform: scale(1.2); } .tip-dot.active { @@ -256,4 +324,30 @@ height: 32px; min-width: 32px; } + + .tip-item { + flex-direction: column; + height: 220px; + } + + .tip-image, .tip-text { + width: 100%; + } + + .tip-image { + height: 120px; + } + + .tip-carousel { + height: 220px; + } +} + +@media (prefers-reduced-motion: reduce) { + .initialization-container, + .tip-item, + .tip-dot { + transition: none; + animation: none; + } } \ No newline at end of file diff --git a/static/js/components/initialization.js b/static/js/components/initialization.js index 1050e8cc..78c48016 100644 --- a/static/js/components/initialization.js +++ b/static/js/components/initialization.js @@ -2,26 +2,38 @@ * Initialization Component * Manages the display of initialization progress and status */ +import { appCore } from '../core.js'; +import { getSessionItem, setSessionItem } from '../utils/storageHelpers.js'; +import { state, getCurrentPageState } from '../state/index.js'; class InitializationManager { constructor() { this.currentTipIndex = 0; this.tipInterval = null; this.websocket = null; - this.currentStage = null; this.progress = 0; - this.stages = [ - { id: 'stageScanFolders', name: 'scan_folders' }, - { id: 'stageCountModels', name: 'count_models' }, - { id: 'stageProcessModels', name: 'process_models' }, - { id: 'stageFinalizing', name: 'finalizing' } - ]; + this.processingStartTime = null; + this.processedFilesCount = 0; + this.totalFilesCount = 0; + this.averageProcessingTime = null; + this.pageType = null; // Added page type property } /** * Initialize the component */ initialize() { + // Initialize core application for theme and header functionality + appCore.initialize().then(() => { + console.log('Core application initialized for initialization component'); + }); + + // Detect the current page type + this.detectPageType(); + + // Check session storage for saved progress + this.restoreProgress(); + // Setup the tip carousel this.setupTipCarousel(); @@ -33,9 +45,65 @@ class InitializationManager { // Show first tip as active document.querySelector('.tip-item').classList.add('active'); - - // Set the first stage as active - this.updateStage('scan_folders'); + } + + /** + * Detect the current page type + */ + detectPageType() { + // Get the current page type from URL or data attribute + const path = window.location.pathname; + if (path.includes('/checkpoints')) { + this.pageType = 'checkpoints'; + } else if (path.includes('/loras')) { + this.pageType = 'loras'; + } else { + // Default to loras if can't determine + this.pageType = 'loras'; + } + console.log(`Initialization component detected page type: ${this.pageType}`); + } + + /** + * Get the storage key with page type prefix + */ + getStorageKey(key) { + return `${this.pageType}_${key}`; + } + + /** + * Restore progress from session storage if available + */ + restoreProgress() { + const savedProgress = getSessionItem(this.getStorageKey('initProgress')); + if (savedProgress) { + console.log(`Restoring ${this.pageType} progress from session storage:`, savedProgress); + + // Restore progress percentage + if (savedProgress.progress !== undefined) { + this.updateProgress(savedProgress.progress); + } + + // Restore processed files count and total files + if (savedProgress.processedFiles !== undefined) { + this.processedFilesCount = savedProgress.processedFiles; + } + + if (savedProgress.totalFiles !== undefined) { + this.totalFilesCount = savedProgress.totalFiles; + } + + // Restore processing time metrics if available + if (savedProgress.averageProcessingTime !== undefined) { + this.averageProcessingTime = savedProgress.averageProcessingTime; + this.updateRemainingTime(); + } + + // Restore progress status message + if (savedProgress.details) { + this.updateStatusMessage(savedProgress.details); + } + } } /** @@ -82,7 +150,7 @@ class InitializationManager { // Set a simulated progress that moves forward slowly // This gives users feedback even if the backend isn't providing updates - let simulatedProgress = 0; + let simulatedProgress = this.progress || 0; const simulateInterval = setInterval(() => { simulatedProgress += 0.5; if (simulatedProgress > 95) { @@ -129,25 +197,97 @@ class InitializationManager { handleProgressUpdate(data) { if (!data) return; + // Check if this update is for our page type + if (data.pageType && data.pageType !== this.pageType) { + console.log(`Ignoring update for ${data.pageType}, we're on ${this.pageType}`); + return; + } + + // If no pageType is specified in the data but we have scanner_type, map it to pageType + if (!data.pageType && data.scanner_type) { + const scannerTypeToPageType = { + 'lora': 'loras', + 'checkpoint': 'checkpoints' + }; + + if (scannerTypeToPageType[data.scanner_type] !== this.pageType) { + console.log(`Ignoring update for ${data.scanner_type}, we're on ${this.pageType}`); + return; + } + } + + // Save progress data to session storage + setSessionItem(this.getStorageKey('initProgress'), { + ...data, + averageProcessingTime: this.averageProcessingTime, + processedFiles: this.processedFilesCount, + totalFiles: this.totalFilesCount + }); + // Update progress percentage if (data.progress !== undefined) { this.updateProgress(data.progress); } - // Update current stage - if (data.stage) { - this.updateStage(data.stage); - } - // Update stage-specific details if (data.details) { - this.updateStageDetails(data.stage, data.details); + this.updateStatusMessage(data.details); + } + + // Track files count for time estimation + if (data.stage === 'count_models' && data.details) { + const match = data.details.match(/Found (\d+)/); + if (match && match[1]) { + this.totalFilesCount = parseInt(match[1]); + } + } + + // Track processed files for time estimation + if (data.stage === 'process_models' && data.details) { + const match = data.details.match(/Processing .* files: (\d+)\/(\d+)/); + if (match && match[1] && match[2]) { + const currentCount = parseInt(match[1]); + const totalCount = parseInt(match[2]); + + // Make sure we have valid total count + if (totalCount > 0 && this.totalFilesCount === 0) { + this.totalFilesCount = totalCount; + } + + // Start tracking processing time once we've processed some files + if (currentCount > 0 && !this.processingStartTime && this.processedFilesCount === 0) { + this.processingStartTime = Date.now(); + } + + // Calculate average processing time based on elapsed time and files processed + if (this.processingStartTime && currentCount > this.processedFilesCount) { + const newFiles = currentCount - this.processedFilesCount; + const elapsedTime = Date.now() - this.processingStartTime; + const timePerFile = elapsedTime / currentCount; // ms per file + + // Update moving average + if (!this.averageProcessingTime) { + this.averageProcessingTime = timePerFile; + } else { + // Simple exponential moving average + this.averageProcessingTime = this.averageProcessingTime * 0.7 + timePerFile * 0.3; + } + + // Update remaining time estimate + this.updateRemainingTime(); + } + + this.processedFilesCount = currentCount; + } } // If initialization is complete, reload the page if (data.status === 'complete') { this.showCompletionMessage(); + // Remove session storage data since we're done + setSessionItem(this.getStorageKey('initProgress'), null); + // Give the user a moment to see the completion message setTimeout(() => { window.location.reload(); @@ -155,6 +295,52 @@ class InitializationManager { } } + /** + * Update the remaining time display based on current progress + */ + updateRemainingTime() { + if (!this.averageProcessingTime || !this.totalFilesCount || this.totalFilesCount <= 0) { + document.getElementById('remainingTime').textContent = 'Estimating...'; + return; + } + + const remainingFiles = this.totalFilesCount - this.processedFilesCount; + const remainingTimeMs = remainingFiles * this.averageProcessingTime; + + if (remainingTimeMs <= 0) { + document.getElementById('remainingTime').textContent = 'Almost done...'; + return; + } + + // Format the time for display + let formattedTime; + if (remainingTimeMs < 60000) { + // Less than a minute + formattedTime = 'Less than a minute'; + } else if (remainingTimeMs < 3600000) { + // Less than an hour + const minutes = Math.round(remainingTimeMs / 60000); + formattedTime = `~${minutes} minute${minutes !== 1 ? 's' : ''}`; + } else { + // Hours and minutes + const hours = Math.floor(remainingTimeMs / 3600000); + const minutes = Math.round((remainingTimeMs % 3600000) / 60000); + formattedTime = `~${hours} hour${hours !== 1 ? 's' : ''} ${minutes} minute${minutes !== 1 ? 's' : ''}`; + } + + document.getElementById('remainingTime').textContent = formattedTime + ' remaining'; + } + + /** + * Update status message + */ + updateStatusMessage(message) { + const progressStatus = document.getElementById('progressStatus'); + if (progressStatus) { + progressStatus.textContent = message; + } + } + /** * Update the progress bar and percentage */ @@ -169,89 +355,6 @@ class InitializationManager { } } - /** - * Update the current stage - */ - updateStage(stageName) { - // Mark the previous stage as completed if it exists - if (this.currentStage) { - const previousStageElement = document.getElementById(this.currentStage); - if (previousStageElement) { - previousStageElement.classList.remove('active'); - previousStageElement.classList.add('completed'); - - // Update the stage status icon to completed - const statusElement = previousStageElement.querySelector('.stage-status'); - if (statusElement) { - statusElement.className = 'stage-status completed'; - statusElement.innerHTML = ''; - } - } - } - - // Find and activate the new current stage - const stageInfo = this.stages.find(s => s.name === stageName); - if (stageInfo) { - this.currentStage = stageInfo.id; - const currentStageElement = document.getElementById(stageInfo.id); - - if (currentStageElement) { - currentStageElement.classList.add('active'); - - // Update the stage status icon to in-progress - const statusElement = currentStageElement.querySelector('.stage-status'); - if (statusElement) { - statusElement.className = 'stage-status in-progress'; - statusElement.innerHTML = ''; - } - - // Update the progress status message - const progressStatus = document.getElementById('progressStatus'); - if (progressStatus) { - progressStatus.textContent = `${this.stageNameToDisplay(stageName)}...`; - } - } - } - } - - /** - * Convert stage name to display text - */ - stageNameToDisplay(stageName) { - switch (stageName) { - case 'scan_folders': - return 'Scanning folders'; - case 'count_models': - return 'Counting models'; - case 'process_models': - return 'Processing models'; - case 'finalizing': - return 'Finalizing'; - default: - return 'Initializing'; - } - } - - /** - * Update stage-specific details - */ - updateStageDetails(stageName, details) { - const detailsMap = { - 'scan_folders': 'scanFoldersDetails', - 'count_models': 'countModelsDetails', - 'process_models': 'processModelsDetails', - 'finalizing': 'finalizingDetails' - }; - - const detailsElementId = detailsMap[stageName]; - if (detailsElementId) { - const detailsElement = document.getElementById(detailsElementId); - if (detailsElement && details) { - detailsElement.textContent = details; - } - } - } - /** * Setup the tip carousel to rotate through tips */ @@ -261,6 +364,7 @@ class InitializationManager { // Show the first tip tipItems[0].classList.add('active'); + document.querySelector('.tip-dot').classList.add('active'); // Set up automatic rotation this.tipInterval = setInterval(() => { @@ -335,33 +439,16 @@ class InitializationManager { * Show completion message */ showCompletionMessage() { - // Mark all stages as completed - this.stages.forEach(stage => { - const stageElement = document.getElementById(stage.id); - if (stageElement) { - stageElement.classList.remove('active'); - stageElement.classList.add('completed'); - - const statusElement = stageElement.querySelector('.stage-status'); - if (statusElement) { - statusElement.className = 'stage-status completed'; - statusElement.innerHTML = ''; - } - } - }); - // Update progress to 100% this.updateProgress(100); // Update status message - const progressStatus = document.getElementById('progressStatus'); - if (progressStatus) { - progressStatus.textContent = 'Initialization complete!'; - } + this.updateStatusMessage('Initialization complete!'); // Update title and subtitle const initTitle = document.getElementById('initTitle'); const initSubtitle = document.getElementById('initSubtitle'); + const remainingTime = document.getElementById('remainingTime'); if (initTitle) { initTitle.textContent = 'Initialization Complete'; @@ -370,6 +457,10 @@ class InitializationManager { if (initSubtitle) { initSubtitle.textContent = 'Reloading page...'; } + + if (remainingTime) { + remainingTime.textContent = 'Done!'; + } } /** diff --git a/templates/components/initialization.html b/templates/components/initialization.html index 4a7f6811..1aa0aa1d 100644 --- a/templates/components/initialization.html +++ b/templates/components/initialization.html @@ -6,98 +6,78 @@
{% block init_message %}Preparing your workspace...{% endblock %}
-