From a043b487bdc437b8a500b56b9a49d993aae99e25 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 13 Apr 2025 10:41:27 +0800 Subject: [PATCH] feat: Add initialization progress WebSocket and UI components - Implement WebSocket route for initialization progress updates - Create initialization component with progress bar and stages - Add styles for initialization UI - Update base template to include initialization component - Enhance model scanner to broadcast progress during initialization --- py/routes/api_routes.py | 1 + py/routes/checkpoints_routes.py | 11 +- py/routes/lora_routes.py | 7 +- py/services/model_scanner.py | 186 ++++++++++- py/services/websocket_manager.py | 32 +- static/css/components/initialization.css | 259 +++++++++++++++ static/css/style.css | 1 + static/js/components/initialization.js | 404 +++++++++++++++++++++++ templates/base.html | 89 +---- templates/components/initialization.html | 104 ++++++ 10 files changed, 996 insertions(+), 98 deletions(-) create mode 100644 static/css/components/initialization.css create mode 100644 static/js/components/initialization.js create mode 100644 templates/components/initialization.html diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index c064d66b..876e67a1 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -46,6 +46,7 @@ class ApiRoutes: app.router.add_get('/api/loras', routes.get_loras) app.router.add_post('/api/fetch-all-civitai', routes.fetch_all_civitai) app.router.add_get('/ws/fetch-progress', ws_manager.handle_connection) + app.router.add_get('/ws/init-progress', ws_manager.handle_init_connection) # Add new WebSocket route app.router.add_get('/api/lora-roots', routes.get_lora_roots) app.router.add_get('/api/folders', routes.get_folders) app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions) diff --git a/py/routes/checkpoints_routes.py b/py/routes/checkpoints_routes.py index 8a947bd6..838adba3 100644 --- a/py/routes/checkpoints_routes.py +++ b/py/routes/checkpoints_routes.py @@ -428,15 +428,16 @@ class CheckpointsRoutes: async def handle_checkpoints_page(self, request: web.Request) -> web.Response: """Handle GET /checkpoints request""" try: - # 检查缓存初始化状态,根据initialize_in_background的工作方式调整判断逻辑 + # Check if the CheckpointScanner is initializing + # It's initializing if the cache object doesn't exist yet, + # OR if the scanner explicitly says it's initializing (background task running). is_initializing = ( - self.scanner._cache is None or - len(self.scanner._cache.raw_data) == 0 or - hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing + self.scanner._cache is None or + (hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing) ) if is_initializing: - # 如果正在初始化,返回一个只包含加载提示的页面 + # If still initializing, return loading page template = self.template_env.get_template('checkpoints.html') rendered = template.render( folders=[], # 空文件夹列表 diff --git a/py/routes/lora_routes.py b/py/routes/lora_routes.py index f22071b1..59504538 100644 --- a/py/routes/lora_routes.py +++ b/py/routes/lora_routes.py @@ -67,10 +67,11 @@ class LoraRoutes: await self.init_services() # Check if the LoraScanner is initializing + # It's initializing if the cache object doesn't exist yet, + # OR if the scanner explicitly says it's initializing (background task running). is_initializing = ( - self.scanner._cache is None or - len(self.scanner._cache.raw_data) == 0 or - hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing + self.scanner._cache is None or + (hasattr(self.scanner, '_is_initializing') and self.scanner._is_initializing) ) if is_initializing: diff --git a/py/services/model_scanner.py b/py/services/model_scanner.py index 0ada59b9..e975d0b4 100644 --- a/py/services/model_scanner.py +++ b/py/services/model_scanner.py @@ -13,6 +13,7 @@ from .model_cache import ModelCache from .model_hash_index import ModelHashIndex from ..utils.constants import PREVIEW_EXTENSIONS from .service_registry import ServiceRegistry +from .websocket_manager import ws_manager logger = logging.getLogger(__name__) @@ -61,21 +62,99 @@ class ModelScanner: # Set initializing flag to true self._is_initializing = True - start_time = time.time() - # Use thread pool to execute CPU-intensive operations + # 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..." + }) + + # Count files in a separate thread to avoid blocking loop = asyncio.get_event_loop() + total_files = await loop.run_in_executor( + None, # Use default thread pool + self._count_model_files # Run file counting in thread + ) + + 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" + }) + + start_time = time.time() + + # Use thread pool to execute CPU-intensive operations with progress reporting await loop.run_in_executor( None, # Use default thread pool - self._initialize_cache_sync # Run synchronous version in thread + self._initialize_cache_sync, # Run synchronous version in thread + total_files # Pass the total file count 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..." + }) + logger.info(f"{self.model_type.capitalize()} cache initialized in {time.time() - start_time:.2f} seconds. Found {len(self._cache.raw_data)} models") + + # Send completion message + await asyncio.sleep(0.5) # Small delay to ensure final progress message is sent + await ws_manager.broadcast_init_progress({ + 'stage': 'finalizing', + 'progress': 100, + 'status': 'complete', + 'details': f"Completed! Found {len(self._cache.raw_data)} {self.model_type} files." + }) + except Exception as e: logger.error(f"{self.model_type.capitalize()} Scanner: Error initializing cache in background: {e}") finally: # Always clear the initializing flag when done self._is_initializing = False - def _initialize_cache_sync(self): + def _count_model_files(self) -> int: + """Count all model files with supported extensions in all roots + + Returns: + int: Total number of model files found + """ + total_files = 0 + visited_real_paths = set() + + for root_path in self.get_model_roots(): + if not os.path.exists(root_path): + continue + + def count_recursive(path): + nonlocal total_files + try: + real_path = os.path.realpath(path) + if real_path in visited_real_paths: + return + visited_real_paths.add(real_path) + + with os.scandir(path) as it: + for entry in it: + try: + if entry.is_file(follow_symlinks=True): + ext = os.path.splitext(entry.name)[1].lower() + if ext in self.file_extensions: + total_files += 1 + elif entry.is_dir(follow_symlinks=True): + count_recursive(entry.path) + except Exception as e: + logger.error(f"Error counting files in entry {entry.path}: {e}") + except Exception as e: + logger.error(f"Error counting files in {path}: {e}") + + count_recursive(root_path) + + return total_files + + def _initialize_cache_sync(self, total_files=0): """Synchronous version of cache initialization for thread pool execution""" try: # Create a new event loop for this thread @@ -84,8 +163,83 @@ class ModelScanner: # Create a synchronous method to bypass the async lock def sync_initialize_cache(): - # Directly call the scan method to avoid lock issues - raw_data = loop.run_until_complete(self.scan_all_models()) + # Track progress + processed_files = 0 + last_progress_time = time.time() + last_progress_percent = 0 + + # We need a wrapper around scan_all_models to track progress + # This is a local function that will run in our thread's event loop + async def scan_with_progress(): + nonlocal processed_files, last_progress_time, last_progress_percent + + # For storing raw model data + all_models = [] + + # Process each model root + for root_path in self.get_model_roots(): + if not os.path.exists(root_path): + continue + + # Track visited paths to avoid symlink loops + visited_paths = set() + + # Recursively process directory + async def scan_dir_with_progress(path): + nonlocal processed_files, last_progress_time, last_progress_percent + + try: + real_path = os.path.realpath(path) + if real_path in visited_paths: + return + visited_paths.add(real_path) + + with os.scandir(path) as it: + entries = list(it) + for entry in entries: + try: + if entry.is_file(follow_symlinks=True): + ext = os.path.splitext(entry.name)[1].lower() + if ext in self.file_extensions: + file_path = entry.path.replace(os.sep, "/") + result = await self._process_model_file(file_path, root_path) + if result: + all_models.append(result) + + # Update progress counter + processed_files += 1 + + # Update progress periodically (not every file to avoid excessive updates) + current_time = time.time() + if total_files > 0 and (current_time - last_progress_time > 0.5 or processed_files == total_files): + # Adjusted progress calculation + progress_percent = min(99, int(1 + (processed_files / total_files) * 98)) + if progress_percent > last_progress_percent: + last_progress_percent = progress_percent + last_progress_time = current_time + + # Send progress update through websocket + await ws_manager.broadcast_init_progress({ + 'stage': 'process_models', + 'progress': progress_percent, + 'details': f"Processing {self.model_type} files: {processed_files}/{total_files}" + }) + + elif entry.is_dir(follow_symlinks=True): + await scan_dir_with_progress(entry.path) + + except Exception as e: + logger.error(f"Error processing entry {entry.path}: {e}") + except Exception as e: + logger.error(f"Error scanning {path}: {e}") + + # Process the root path + await scan_dir_with_progress(root_path) + + return all_models + + # Run the progress-tracking scan function + raw_data = loop.run_until_complete(scan_with_progress()) # Update hash index and tags count for model_data in raw_data: @@ -136,6 +290,7 @@ class ModelScanner: async def _initialize_cache(self) -> None: """Initialize or refresh the cache""" + self._is_initializing = True # Set flag try: start_time = time.time() # Clear existing hash index @@ -171,15 +326,20 @@ class ModelScanner: logger.info(f"{self.model_type.capitalize()} Scanner: Cache initialization completed in {time.time() - start_time:.2f} seconds, found {len(raw_data)} models") except Exception as e: logger.error(f"{self.model_type.capitalize()} Scanner: Error initializing cache: {e}") - self._cache = ModelCache( - raw_data=[], - sorted_by_name=[], - sorted_by_date=[], - folders=[] - ) + # Ensure cache is at least an empty structure on error + if self._cache is None: + self._cache = ModelCache( + raw_data=[], + sorted_by_name=[], + sorted_by_date=[], + folders=[] + ) + finally: + self._is_initializing = False # Unset flag async def _reconcile_cache(self) -> None: """Fast cache reconciliation - only process differences between cache and filesystem""" + self._is_initializing = True # Set flag for reconciliation duration try: start_time = time.time() logger.info(f"{self.model_type.capitalize()} Scanner: Starting fast cache reconciliation...") @@ -306,6 +466,8 @@ class ModelScanner: logger.info(f"{self.model_type.capitalize()} Scanner: Cache reconciliation completed in {time.time() - start_time:.2f} seconds. Added {total_added}, removed {total_removed} models.") except Exception as e: logger.error(f"{self.model_type.capitalize()} Scanner: Error reconciling cache: {e}", exc_info=True) + finally: + self._is_initializing = False # Unset flag # These methods should be implemented in child classes async def scan_all_models(self) -> List[Dict]: diff --git a/py/services/websocket_manager.py b/py/services/websocket_manager.py index fccd5e41..7f41d85b 100644 --- a/py/services/websocket_manager.py +++ b/py/services/websocket_manager.py @@ -9,6 +9,7 @@ class WebSocketManager: def __init__(self): self._websockets: Set[web.WebSocketResponse] = set() + self._init_websockets: Set[web.WebSocketResponse] = set() # New set for initialization progress clients async def handle_connection(self, request: web.Request) -> web.WebSocketResponse: """Handle new WebSocket connection""" @@ -23,6 +24,20 @@ class WebSocketManager: finally: self._websockets.discard(ws) return ws + + async def handle_init_connection(self, request: web.Request) -> web.WebSocketResponse: + """Handle new WebSocket connection for initialization progress""" + ws = web.WebSocketResponse() + await ws.prepare(request) + self._init_websockets.add(ws) + + try: + async for msg in ws: + if msg.type == web.WSMsgType.ERROR: + logger.error(f'Init WebSocket error: {ws.exception()}') + finally: + self._init_websockets.discard(ws) + return ws async def broadcast(self, data: Dict): """Broadcast message to all connected clients""" @@ -34,10 +49,25 @@ class WebSocketManager: await ws.send_json(data) except Exception as e: logger.error(f"Error sending progress: {e}") + + async def broadcast_init_progress(self, data: Dict): + """Broadcast initialization progress to connected clients""" + if not self._init_websockets: + return + + for ws in self._init_websockets: + try: + await ws.send_json(data) + except Exception as e: + logger.error(f"Error sending initialization progress: {e}") def get_connected_clients_count(self) -> int: """Get number of connected clients""" return len(self._websockets) + def get_init_clients_count(self) -> int: + """Get number of initialization progress clients""" + return len(self._init_websockets) + # Global instance -ws_manager = WebSocketManager() \ No newline at end of file +ws_manager = WebSocketManager() \ No newline at end of file diff --git a/static/css/components/initialization.css b/static/css/components/initialization.css new file mode 100644 index 00000000..a6a98eca --- /dev/null +++ b/static/css/components/initialization.css @@ -0,0 +1,259 @@ +/* Initialization Component Styles */ + +.initialization-container { + width: 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; +} + +.initialization-content { + max-width: 800px; + margin: 0 auto; +} + +.initialization-header { + text-align: center; + margin-bottom: var(--space-3); +} + +.initialization-header h2 { + font-size: 1.8rem; + margin-bottom: var(--space-1); + color: var(--text-color); +} + +.init-subtitle { + color: var(--text-color); + opacity: 0.8; + font-size: 1rem; +} + +/* Progress Bar Styles */ +.initialization-progress { + margin-bottom: var(--space-3); +} + +.progress-bar-container { + width: 100%; + height: 8px; + background-color: var(--lora-border); + border-radius: 4px; + overflow: hidden; + margin-bottom: var(--space-1); +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, var(--lora-accent) 0%, rgba(var(--lora-accent), 0.8) 100%); + border-radius: 4px; + transition: width 0.3s ease; + width: 0%; +} + +.progress-details { + display: flex; + justify-content: space-between; + font-size: 0.9rem; + color: var(--text-color); +} + +/* Stages Styles */ +.initialization-stages { + margin-bottom: var(--space-3); +} + +.stage-item { + display: flex; + align-items: flex-start; + padding: var(--space-2); + border-radius: var(--border-radius-xs); + margin-bottom: var(--space-1); + transition: background-color 0.2s ease; + border: 1px solid transparent; +} + +.stage-item.active { + background-color: rgba(var(--lora-accent), 0.1); + border-color: var(--lora-accent); +} + +.stage-item.completed { + background-color: rgba(0, 150, 0, 0.05); + border-color: rgba(0, 150, 0, 0.2); +} + +.stage-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: var(--lora-border); + border-radius: 50%; + margin-right: var(--space-2); +} + +.stage-item.active .stage-icon { + background: var(--lora-accent); + color: white; +} + +.stage-item.completed .stage-icon { + background: rgb(0, 150, 0); + color: white; +} + +.stage-content { + flex: 1; +} + +.stage-content h4 { + margin: 0 0 5px 0; + font-size: 1rem; + color: var(--text-color); +} + +.stage-details { + font-size: 0.85rem; + color: var(--text-color); + opacity: 0.8; +} + +.stage-status { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +} + +.stage-status.pending { + color: var(--text-color); + opacity: 0.5; +} + +.stage-status.in-progress { + color: var(--lora-accent); +} + +.stage-status.completed { + color: rgb(0, 150, 0); +} + +/* Tips Styles */ +.initialization-tips { + background: rgba(var(--lora-accent), 0.05); + border-radius: var(--border-radius-base); + padding: var(--space-2); +} + +.tips-header { + display: flex; + align-items: center; + margin-bottom: var(--space-1); + color: var(--text-color); +} + +.tips-header i { + margin-right: 8px; + color: var(--lora-accent); +} + +.tips-header h3 { + font-size: 1.1rem; + margin: 0; +} + +.tip-carousel { + position: relative; + height: 60px; + overflow: hidden; +} + +.tip-item { + position: absolute; + width: 100%; + opacity: 0; + transition: opacity 0.5s ease; + padding: 0 var(--space-1); +} + +.tip-item.active { + opacity: 1; +} + +.tip-item p { + margin: 0; + line-height: 1.5; + font-size: 0.9rem; + color: var(--text-color); +} + +.tip-navigation { + display: flex; + justify-content: center; + margin-top: var(--space-1); +} + +.tip-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--lora-border); + margin: 0 4px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.tip-dot.active { + background-color: var(--lora-accent); +} + +/* Animation */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Different stage status animations */ +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +} + +.stage-item.active .stage-icon i { + animation: pulse 1s infinite; +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .initialization-container { + padding: var(--space-2); + } + + .stage-item { + padding: var(--space-1); + } + + .stage-icon { + width: 32px; + height: 32px; + min-width: 32px; + } +} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index 400f7d07..21effde0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -19,6 +19,7 @@ @import 'components/bulk.css'; @import 'components/shared.css'; @import 'components/filter-indicator.css'; +@import 'components/initialization.css'; .initialization-notice { display: flex; diff --git a/static/js/components/initialization.js b/static/js/components/initialization.js new file mode 100644 index 00000000..1050e8cc --- /dev/null +++ b/static/js/components/initialization.js @@ -0,0 +1,404 @@ +/** + * Initialization Component + * Manages the display of initialization progress and status + */ + +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' } + ]; + } + + /** + * Initialize the component + */ + initialize() { + // Setup the tip carousel + this.setupTipCarousel(); + + // Connect to WebSocket for progress updates + this.connectWebSocket(); + + // Add event listeners for tip navigation + this.setupTipNavigation(); + + // Show first tip as active + document.querySelector('.tip-item').classList.add('active'); + + // Set the first stage as active + this.updateStage('scan_folders'); + } + + /** + * Connect to WebSocket for initialization progress updates + */ + connectWebSocket() { + try { + const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; + this.websocket = new WebSocket(`${wsProtocol}${window.location.host}/ws/init-progress`); + + this.websocket.onopen = () => { + console.log('Connected to initialization progress WebSocket'); + }; + + this.websocket.onmessage = (event) => { + this.handleProgressUpdate(JSON.parse(event.data)); + }; + + this.websocket.onerror = (error) => { + console.error('WebSocket error:', error); + // Fall back to polling if WebSocket fails + this.fallbackToPolling(); + }; + + this.websocket.onclose = () => { + console.log('WebSocket connection closed'); + // Check if we need to fall back to polling + if (!this.pollingActive) { + this.fallbackToPolling(); + } + }; + } catch (error) { + console.error('Failed to connect to WebSocket:', error); + this.fallbackToPolling(); + } + } + + /** + * Fall back to polling if WebSocket connection fails + */ + fallbackToPolling() { + this.pollingActive = true; + this.pollProgress(); + + // Set a simulated progress that moves forward slowly + // This gives users feedback even if the backend isn't providing updates + let simulatedProgress = 0; + const simulateInterval = setInterval(() => { + simulatedProgress += 0.5; + if (simulatedProgress > 95) { + clearInterval(simulateInterval); + return; + } + + // Only use simulated progress if we haven't received a real update + if (this.progress < simulatedProgress) { + this.updateProgress(simulatedProgress); + } + }, 1000); + } + + /** + * Poll for progress updates from the server + */ + pollProgress() { + const checkProgress = () => { + fetch('/api/init-status') + .then(response => response.json()) + .then(data => { + this.handleProgressUpdate(data); + + // If initialization is complete, stop polling + if (data.status !== 'complete') { + setTimeout(checkProgress, 2000); + } else { + window.location.reload(); + } + }) + .catch(error => { + console.error('Error polling for progress:', error); + setTimeout(checkProgress, 3000); // Try again after a longer delay + }); + }; + + checkProgress(); + } + + /** + * Handle progress updates from WebSocket or polling + */ + handleProgressUpdate(data) { + if (!data) return; + + // 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); + } + + // If initialization is complete, reload the page + if (data.status === 'complete') { + this.showCompletionMessage(); + + // Give the user a moment to see the completion message + setTimeout(() => { + window.location.reload(); + }, 1500); + } + } + + /** + * Update the progress bar and percentage + */ + updateProgress(progress) { + this.progress = progress; + const progressBar = document.getElementById('initProgressBar'); + const progressPercentage = document.getElementById('progressPercentage'); + + if (progressBar && progressPercentage) { + progressBar.style.width = `${progress}%`; + progressPercentage.textContent = `${Math.round(progress)}%`; + } + } + + /** + * 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 + */ + setupTipCarousel() { + const tipItems = document.querySelectorAll('.tip-item'); + if (tipItems.length === 0) return; + + // Show the first tip + tipItems[0].classList.add('active'); + + // Set up automatic rotation + this.tipInterval = setInterval(() => { + this.showNextTip(); + }, 8000); // Change tip every 8 seconds + } + + /** + * Setup tip navigation dots + */ + setupTipNavigation() { + const tipDots = document.querySelectorAll('.tip-dot'); + + tipDots.forEach((dot, index) => { + dot.addEventListener('click', () => { + this.showTipByIndex(index); + }); + }); + } + + /** + * Show the next tip in the carousel + */ + showNextTip() { + const tipItems = document.querySelectorAll('.tip-item'); + const tipDots = document.querySelectorAll('.tip-dot'); + + if (tipItems.length === 0) return; + + // Hide current tip + tipItems[this.currentTipIndex].classList.remove('active'); + tipDots[this.currentTipIndex].classList.remove('active'); + + // Calculate next index + this.currentTipIndex = (this.currentTipIndex + 1) % tipItems.length; + + // Show next tip + tipItems[this.currentTipIndex].classList.add('active'); + tipDots[this.currentTipIndex].classList.add('active'); + } + + /** + * Show a specific tip by index + */ + showTipByIndex(index) { + const tipItems = document.querySelectorAll('.tip-item'); + const tipDots = document.querySelectorAll('.tip-dot'); + + if (index >= tipItems.length || index < 0) return; + + // Hide current tip + tipItems[this.currentTipIndex].classList.remove('active'); + tipDots[this.currentTipIndex].classList.remove('active'); + + // Update index and show selected tip + this.currentTipIndex = index; + + // Show selected tip + tipItems[this.currentTipIndex].classList.add('active'); + tipDots[this.currentTipIndex].classList.add('active'); + + // Reset interval to prevent quick tip change + if (this.tipInterval) { + clearInterval(this.tipInterval); + this.tipInterval = setInterval(() => { + this.showNextTip(); + }, 8000); + } + } + + /** + * 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!'; + } + + // Update title and subtitle + const initTitle = document.getElementById('initTitle'); + const initSubtitle = document.getElementById('initSubtitle'); + + if (initTitle) { + initTitle.textContent = 'Initialization Complete'; + } + + if (initSubtitle) { + initSubtitle.textContent = 'Reloading page...'; + } + } + + /** + * Clean up resources when the component is destroyed + */ + cleanup() { + if (this.tipInterval) { + clearInterval(this.tipInterval); + } + + if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { + this.websocket.close(); + } + } +} + +// Create and export the initialization manager +export const initManager = new InitializationManager(); + +// Initialize the component when the DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + // Only initialize if we're in initialization mode + const initContainer = document.getElementById('initializationContainer'); + if (initContainer) { + initManager.initialize(); + } +}); + +// Clean up when the page is unloaded +window.addEventListener('beforeunload', () => { + initManager.cleanup(); +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index e9c283aa..30f6b468 100644 --- a/templates/base.html +++ b/templates/base.html @@ -37,49 +37,6 @@ - - {% if is_initializing %} - - {% endif %} - + + + {% else %} + {% block main_script %}{% endblock %} {% endif %} {% block additional_scripts %}{% endblock %} diff --git a/templates/components/initialization.html b/templates/components/initialization.html new file mode 100644 index 00000000..4a7f6811 --- /dev/null +++ b/templates/components/initialization.html @@ -0,0 +1,104 @@ + +
+
+
+

{% block init_title %}Initializing{% endblock %}

+

{% block init_message %}Preparing your workspace...{% endblock %}

+
+ +
+
+
+
+
+ 0% + Starting initialization... +
+
+ +
+
+
+ +
+
+

Scanning Folders

+
Discovering model directories...
+
+
+ +
+
+ +
+
+ +
+
+

Counting Models

+
Analyzing files...
+
+
+ +
+
+ +
+
+ +
+
+

Processing Models

+
Reading model metadata...
+
+
+ +
+
+ +
+
+ +
+
+

Finalizing

+
Building cache and optimizing...
+
+
+ +
+
+
+ +
+
+ +

Tips

+
+ +
+ + + + + +
+
+
+
\ No newline at end of file