From bae66f94e8bb54024a81ffd3cf26b667617e7124 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 29 May 2025 15:51:45 +0800 Subject: [PATCH] Add full rebuild option to model refresh functionality and enhance dropdown controls --- py/routes/api_routes.py | 7 +- py/routes/checkpoints_routes.py | 5 +- static/css/layout.css | 65 +++++++++++++++++ static/js/api/baseModelApi.js | 15 ++-- static/js/api/checkpointApi.js | 5 +- static/js/api/loraApi.js | 5 +- .../controls/CheckpointsControls.js | 4 +- .../js/components/controls/LorasControls.js | 4 +- static/js/components/controls/PageControls.js | 69 +++++++++++++++++-- templates/components/controls.html | 15 +++- 10 files changed, 171 insertions(+), 23 deletions(-) diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index b2eece51..49f9aadf 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -106,8 +106,11 @@ class ApiRoutes: async def scan_loras(self, request: web.Request) -> web.Response: """Force a rescan of LoRA files""" - try: - await self.scanner.get_cached_data(force_refresh=True) + try: + # Get full_rebuild parameter from query string, default to false + full_rebuild = request.query.get('full_rebuild', 'false').lower() == 'true' + + await self.scanner.get_cached_data(force_refresh=True, rebuild_cache=full_rebuild) return web.json_response({"status": "success", "message": "LoRA scan completed"}) except Exception as e: logger.error(f"Error in scan_loras: {e}", exc_info=True) diff --git a/py/routes/checkpoints_routes.py b/py/routes/checkpoints_routes.py index 1438c630..9d7936f8 100644 --- a/py/routes/checkpoints_routes.py +++ b/py/routes/checkpoints_routes.py @@ -420,7 +420,10 @@ class CheckpointsRoutes: async def scan_checkpoints(self, request): """Force a rescan of checkpoint files""" try: - await self.scanner.get_cached_data(force_refresh=True) + # Get the full_rebuild parameter and convert to bool, default to False + full_rebuild = request.query.get('full_rebuild', 'false').lower() == 'true' + + await self.scanner.get_cached_data(force_refresh=True, rebuild_cache=full_rebuild) return web.json_response({"status": "success", "message": "Checkpoint scan completed"}) except Exception as e: logger.error(f"Error in scan_checkpoints: {e}", exc_info=True) diff --git a/static/css/layout.css b/static/css/layout.css index 8eb4ff72..de003dbd 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -333,6 +333,66 @@ user-select: none; } +/* Dropdown Button Styling */ +.dropdown-group { + position: relative; + display: flex; +} + +.dropdown-main { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 1px solid rgba(0, 0, 0, 0.1); +} + +.dropdown-toggle { + width: 24px !important; + min-width: unset !important; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding: 0 !important; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + min-width: 200px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 0.85em; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} + +.dropdown-group.active .dropdown-menu { + display: block; +} + +.dropdown-item { + display: block; + padding: 6px 15px; + clear: both; + font-weight: 400; + color: var(--text-color); + cursor: pointer; + transition: background-color 0.2s ease; +} + +.dropdown-item:hover { + background-color: oklch(var(--lora-accent) / 0.1); +} + +.dropdown-item i { + margin-right: 8px; + width: 16px; + text-align: center; +} + @media (max-width: 768px) { .actions { flex-wrap: wrap; @@ -378,4 +438,9 @@ .back-to-top { bottom: 60px; /* Give some extra space from bottom on mobile */ } + + .dropdown-menu { + left: auto; + right: 0; /* Align to right on mobile */ + } } diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 0227f692..af9cfafb 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -499,13 +499,18 @@ export async function refreshModels(options = {}) { const { modelType = 'lora', scanEndpoint = '/api/loras/scan', - resetAndReloadFunction + resetAndReloadFunction, + fullRebuild = false // New parameter with default value false } = options; try { - state.loadingManager.showSimpleLoading(`Refreshing ${modelType}s...`); + state.loadingManager.showSimpleLoading(`${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${modelType}s...`); - const response = await fetch(scanEndpoint); + // Add fullRebuild parameter to the request + const url = new URL(scanEndpoint, window.location.origin); + url.searchParams.append('full_rebuild', fullRebuild); + + const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to refresh ${modelType}s: ${response.status} ${response.statusText}`); @@ -515,10 +520,10 @@ export async function refreshModels(options = {}) { await resetAndReloadFunction(true); // update folders } - showToast(`Refresh complete`, 'success'); + showToast(`${fullRebuild ? 'Full rebuild' : 'Refresh'} complete`, 'success'); } catch (error) { console.error(`Refresh failed:`, error); - showToast(`Failed to refresh ${modelType}s`, 'error'); + showToast(`Failed to ${fullRebuild ? 'rebuild' : 'refresh'} ${modelType}s`, 'error'); } finally { state.loadingManager.hide(); state.loadingManager.restoreProgressBar(); diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js index ea124ea9..957ad42c 100644 --- a/static/js/api/checkpointApi.js +++ b/static/js/api/checkpointApi.js @@ -76,11 +76,12 @@ export async function resetAndReload(updateFolders = false) { } // Refresh checkpoints -export async function refreshCheckpoints() { +export async function refreshCheckpoints(fullRebuild = false) { return baseRefreshModels({ modelType: 'checkpoint', scanEndpoint: '/api/checkpoints/scan', - resetAndReloadFunction: resetAndReload + resetAndReloadFunction: resetAndReload, + fullRebuild: fullRebuild }); } diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 7cd496cc..60ac962f 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -143,11 +143,12 @@ export async function resetAndReload(updateFolders = false) { } } -export async function refreshLoras() { +export async function refreshLoras(fullRebuild = false) { return baseRefreshModels({ modelType: 'lora', scanEndpoint: '/api/loras/scan', - resetAndReloadFunction: resetAndReload + resetAndReloadFunction: resetAndReload, + fullRebuild: fullRebuild }); } diff --git a/static/js/components/controls/CheckpointsControls.js b/static/js/components/controls/CheckpointsControls.js index 8cc323f1..f5af38a8 100644 --- a/static/js/components/controls/CheckpointsControls.js +++ b/static/js/components/controls/CheckpointsControls.js @@ -33,8 +33,8 @@ export class CheckpointsControls extends PageControls { return await resetAndReload(updateFolders); }, - refreshModels: async () => { - return await refreshCheckpoints(); + refreshModels: async (fullRebuild = false) => { + return await refreshCheckpoints(fullRebuild); }, // Add fetch from Civitai functionality for checkpoints diff --git a/static/js/components/controls/LorasControls.js b/static/js/components/controls/LorasControls.js index 0fd26af0..22f768d4 100644 --- a/static/js/components/controls/LorasControls.js +++ b/static/js/components/controls/LorasControls.js @@ -36,8 +36,8 @@ export class LorasControls extends PageControls { return await resetAndReload(updateFolders); }, - refreshModels: async () => { - return await refreshLoras(); + refreshModels: async (fullRebuild = false) => { + return await refreshLoras(fullRebuild); }, // LoRA-specific API functions diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js index 9569a639..2e22df48 100644 --- a/static/js/components/controls/PageControls.js +++ b/static/js/components/controls/PageControls.js @@ -83,9 +83,12 @@ export class PageControls { // Refresh button handler const refreshBtn = document.querySelector('[data-action="refresh"]'); if (refreshBtn) { - refreshBtn.addEventListener('click', () => this.refreshModels()); + refreshBtn.addEventListener('click', () => this.refreshModels(false)); // Regular refresh (incremental) } + // Initialize dropdown functionality + this.initDropdowns(); + // Toggle folders button const toggleFoldersBtn = document.querySelector('.toggle-folders-btn'); if (toggleFoldersBtn) { @@ -102,6 +105,61 @@ export class PageControls { this.initPageSpecificListeners(); } + /** + * Initialize dropdown functionality + */ + initDropdowns() { + // Handle dropdown toggles + const dropdownToggles = document.querySelectorAll('.dropdown-toggle'); + dropdownToggles.forEach(toggle => { + toggle.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent triggering parent button + const dropdownGroup = toggle.closest('.dropdown-group'); + + // Close all other open dropdowns first + document.querySelectorAll('.dropdown-group.active').forEach(group => { + if (group !== dropdownGroup) { + group.classList.remove('active'); + } + }); + + // Toggle current dropdown + dropdownGroup.classList.toggle('active'); + }); + }); + + // Handle quick refresh option + const quickRefreshOption = document.querySelector('[data-action="quick-refresh"]'); + if (quickRefreshOption) { + quickRefreshOption.addEventListener('click', (e) => { + e.stopPropagation(); + this.refreshModels(false); + // Close the dropdown + document.querySelector('.dropdown-group.active')?.classList.remove('active'); + }); + } + + // Handle full rebuild option + const fullRebuildOption = document.querySelector('[data-action="full-rebuild"]'); + if (fullRebuildOption) { + fullRebuildOption.addEventListener('click', (e) => { + e.stopPropagation(); + this.refreshModels(true); + // Close the dropdown + document.querySelector('.dropdown-group.active')?.classList.remove('active'); + }); + } + + // Close dropdowns when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('.dropdown-group')) { + document.querySelectorAll('.dropdown-group.active').forEach(group => { + group.classList.remove('active'); + }); + } + }); + } + /** * Initialize page-specific event listeners */ @@ -327,18 +385,19 @@ export class PageControls { /** * Refresh models list + * @param {boolean} fullRebuild - Whether to perform a full rebuild */ - async refreshModels() { + async refreshModels(fullRebuild = false) { if (!this.api) { console.error('API methods not registered'); return; } try { - await this.api.refreshModels(); + await this.api.refreshModels(fullRebuild); } catch (error) { - console.error(`Error refreshing ${this.pageType}:`, error); - showToast(`Failed to refresh ${this.pageType}: ${error.message}`, 'error'); + console.error(`Error ${fullRebuild ? 'rebuilding' : 'refreshing'} ${this.pageType}:`, error); + showToast(`Failed to ${fullRebuild ? 'rebuild' : 'refresh'} ${this.pageType}: ${error.message}`, 'error'); } } diff --git a/templates/components/controls.html b/templates/components/controls.html index 9d946619..6b7c69be 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -15,8 +15,19 @@ -