feat: Enhance initialization component with progress tracking and UI improvements

This commit is contained in:
Will Miao
2025-04-13 12:58:38 +08:00
parent a043b487bd
commit 0b11e6e6d0
5 changed files with 414 additions and 224 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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 = '<i class="fas fa-check"></i>';
}
}
}
// 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 = '<i class="fas fa-spinner fa-spin"></i>';
}
// 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 = '<i class="fas fa-check"></i>';
}
}
});
// 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!';
}
}
/**

View File

@@ -6,98 +6,78 @@
<p class="init-subtitle" id="initSubtitle">{% block init_message %}Preparing your workspace...{% endblock %}</p>
</div>
<div class="initialization-progress">
<div class="progress-bar-container">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-status" id="progressStatus">Initializing...</div>
<div class="progress-container">
<div class="progress-bar" id="initProgressBar"></div>
</div>
<div class="progress-details">
<span class="progress-percentage" id="progressPercentage">0%</span>
<span class="progress-status" id="progressStatus">Starting initialization...</span>
<span id="progressPercentage">0%</span>
<span id="remainingTime">Estimating time...</span>
</div>
</div>
<div class="initialization-stages">
<div class="stage-item" id="stageScanFolders">
<div class="stage-icon">
<i class="fas fa-folder-open"></i>
</div>
<div class="stage-content">
<h4>Scanning Folders</h4>
<div class="stage-details" id="scanFoldersDetails">Discovering model directories...</div>
</div>
<div class="stage-status pending">
<i class="fas fa-clock"></i>
</div>
</div>
<div class="stage-item" id="stageCountModels">
<div class="stage-icon">
<i class="fas fa-calculator"></i>
</div>
<div class="stage-content">
<h4>Counting Models</h4>
<div class="stage-details" id="countModelsDetails">Analyzing files...</div>
</div>
<div class="stage-status pending">
<i class="fas fa-clock"></i>
</div>
</div>
<div class="stage-item" id="stageProcessModels">
<div class="stage-icon">
<i class="fas fa-cogs"></i>
</div>
<div class="stage-content">
<h4>Processing Models</h4>
<div class="stage-details" id="processModelsDetails">Reading model metadata...</div>
</div>
<div class="stage-status pending">
<i class="fas fa-clock"></i>
</div>
</div>
<div class="stage-item" id="stageFinalizing">
<div class="stage-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="stage-content">
<h4>Finalizing</h4>
<div class="stage-details" id="finalizingDetails">Building cache and optimizing...</div>
</div>
<div class="stage-status pending">
<i class="fas fa-clock"></i>
</div>
</div>
</div>
<div class="initialization-tips">
<div class="tips-container">
<div class="tips-header">
<i class="fas fa-lightbulb"></i>
<h3>Tips</h3>
<h3>Tips & Tricks</h3>
</div>
<div class="tip-carousel" id="tipCarousel">
<div class="tip-item">
<p>You can drag and drop LoRA files into your folders to automatically import them.</p>
<div class="tips-content">
<div class="tip-carousel" id="tipCarousel">
<div class="tip-item active">
<div class="tip-image">
<img src="/static/images/tips/drag-drop.png" alt="Drag and Drop" onerror="this.src='/static/images/no-preview.png'">
</div>
<div class="tip-text">
<h4>Quick Import</h4>
<p>You can drag and drop LoRA files into your folders to automatically import them.</p>
</div>
</div>
<div class="tip-item">
<div class="tip-image">
<img src="/static/images/tips/civitai-download.png" alt="Civitai Download" onerror="this.src='/static/images/no-preview.png'">
</div>
<div class="tip-text">
<h4>Easy Download</h4>
<p>Use Civitai URLs to quickly download and install new models.</p>
</div>
</div>
<div class="tip-item">
<div class="tip-image">
<img src="/static/images/tips/recipes.png" alt="Recipes" onerror="this.src='/static/images/no-preview.png'">
</div>
<div class="tip-text">
<h4>Save Recipes</h4>
<p>Create recipes to save your favorite model combinations for future use.</p>
</div>
</div>
<div class="tip-item">
<div class="tip-image">
<img src="/static/images/tips/filter.png" alt="Filter Models" onerror="this.src='/static/images/no-preview.png'">
</div>
<div class="tip-text">
<h4>Fast Filtering</h4>
<p>Filter models by tags or base model type using the filter button in the header.</p>
</div>
</div>
<div class="tip-item">
<div class="tip-image">
<img src="/static/images/tips/search.png" alt="Quick Search" onerror="this.src='/static/images/no-preview.png'">
</div>
<div class="tip-text">
<h4>Quick Search</h4>
<p>Press Ctrl+F (Cmd+F on Mac) to quickly search within your current view.</p>
</div>
</div>
</div>
<div class="tip-item">
<p>Use Civitai URLs to quickly download and install new models.</p>
<div class="tip-navigation">
<span class="tip-dot active" data-index="0"></span>
<span class="tip-dot" data-index="1"></span>
<span class="tip-dot" data-index="2"></span>
<span class="tip-dot" data-index="3"></span>
<span class="tip-dot" data-index="4"></span>
</div>
<div class="tip-item">
<p>Create recipes to save your favorite model combinations for future use.</p>
</div>
<div class="tip-item">
<p>Filter models by tags or base model type using the filter button in the header.</p>
</div>
<div class="tip-item">
<p>Press Ctrl+F (Cmd+F on Mac) to quickly search within your current view.</p>
</div>
</div>
<div class="tip-navigation">
<span class="tip-dot active" data-index="0"></span>
<span class="tip-dot" data-index="1"></span>
<span class="tip-dot" data-index="2"></span>
<span class="tip-dot" data-index="3"></span>
<span class="tip-dot" data-index="4"></span>
</div>
</div>
</div>