From 611dd33c7512c88b71fb5f3321f84b03b6d73b1d Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sat, 3 May 2025 15:43:04 +0800 Subject: [PATCH] feat: add model exclution functionality frontend --- py/routes/api_routes.py | 2 +- static/css/components/modal.css | 32 ++++++------ static/js/api/baseModelApi.js | 50 ++++++++++++++++--- static/js/api/checkpointApi.js | 12 ++++- static/js/api/loraApi.js | 12 ++++- static/js/checkpoints.js | 4 +- static/js/components/CheckpointCard.js | 13 +---- .../ContextMenu/CheckpointContextMenu.js | 5 ++ .../components/ContextMenu/LoraContextMenu.js | 4 ++ static/js/loras.js | 4 +- static/js/managers/ModalManager.js | 15 +++++- static/js/utils/modalUtils.js | 49 +++++++++++++++++- templates/checkpoints.html | 1 + templates/components/context_menu.html | 3 ++ templates/components/modals.html | 13 +++++ 15 files changed, 175 insertions(+), 44 deletions(-) diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index b7bba034..83d791a8 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -43,7 +43,7 @@ class ApiRoutes: app.on_startup.append(lambda _: routes.initialize_services()) app.router.add_post('/api/delete_model', routes.delete_model) - app.router.add_post('/api/exclude_model', routes.exclude_model) # Add new exclude endpoint + app.router.add_post('/api/loras/exclude', routes.exclude_model) # Add new exclude endpoint app.router.add_post('/api/fetch-civitai', routes.fetch_civitai) app.router.add_post('/api/replace_preview', routes.replace_preview) app.router.add_get('/api/loras', routes.get_loras) diff --git a/static/css/components/modal.css b/static/css/components/modal.css index e51ba923..09e3e12b 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -44,26 +44,12 @@ body.modal-open { } /* Delete Modal specific styles */ -.delete-modal-content { - max-width: 500px; - text-align: center; -} .delete-message { color: var(--text-color); margin: var(--space-2) 0; } -.delete-model-info { - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); - padding: var(--space-2); - margin: var(--space-2) 0; - color: var(--text-color); - word-break: break-all; -} - /* Update delete modal styles */ .delete-modal { display: none; /* Set initial display to none */ @@ -92,7 +78,8 @@ body.modal-open { animation: modalFadeIn 0.2s ease-out; } -.delete-model-info { +.delete-model-info, +.exclude-model-info { /* Update info display styling */ background: var(--lora-surface); border: 1px solid var(--lora-border); @@ -123,7 +110,7 @@ body.modal-open { margin-top: var(--space-3); } -.cancel-btn, .delete-btn { +.cancel-btn, .delete-btn, .exclude-btn { padding: 8px var(--space-2); border-radius: 6px; border: none; @@ -143,6 +130,12 @@ body.modal-open { color: white; } +/* Style for exclude button - different from delete button */ +.exclude-btn { + background: var(--lora-accent, #4f46e5); + color: white; +} + .cancel-btn:hover { background: var(--lora-border); } @@ -151,6 +144,11 @@ body.modal-open { opacity: 0.9; } +.exclude-btn:hover { + opacity: 0.9; + background: oklch(from var(--lora-accent, #4f46e5) l c h / 85%); +} + .modal-content h2 { color: var(--text-color); margin-bottom: var(--space-2); @@ -587,7 +585,7 @@ input:checked + .toggle-slider:before { border-radius: var(--border-radius-xs); border: 1px solid var(--border-color); background-color: var(--lora-surface); - color: var(--text-color); + color: var (--text-color); font-size: 0.95em; height: 32px; } diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 60bc4b8a..7991b52e 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -209,13 +209,7 @@ export function replaceModelPreview(filePath, modelType = 'lora') { // Delete a model (generic) export function deleteModel(filePath, modelType = 'lora') { - if (modelType === 'checkpoint') { - confirmDelete('Are you sure you want to delete this checkpoint?', () => { - performDelete(filePath, modelType); - }); - } else { - showDeleteModal(filePath); - } + showDeleteModal(filePath); } // Reset and reload models @@ -394,6 +388,48 @@ export async function refreshSingleModelMetadata(filePath, modelType = 'lora') { } } +// Generic function to exclude a model +export async function excludeModel(filePath, modelType = 'lora') { + try { + const endpoint = modelType === 'checkpoint' + ? '/api/checkpoints/exclude' + : '/api/loras/exclude'; + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_path: filePath + }) + }); + + if (!response.ok) { + throw new Error(`Failed to exclude ${modelType}: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.success) { + // Remove the card from UI + const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); + if (card) { + card.remove(); + } + + showToast(`${modelType} excluded successfully`, 'success'); + return true; + } else { + throw new Error(data.error || `Failed to exclude ${modelType}`); + } + } catch (error) { + console.error(`Error excluding ${modelType}:`, error); + showToast(`Failed to exclude ${modelType}: ${error.message}`, 'error'); + return false; + } +} + // Private methods // Upload a preview image diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js index a1a59c96..c5ebcd3f 100644 --- a/static/js/api/checkpointApi.js +++ b/static/js/api/checkpointApi.js @@ -6,7 +6,8 @@ import { deleteModel as baseDeleteModel, replaceModelPreview, fetchCivitaiMetadata, - refreshSingleModelMetadata + refreshSingleModelMetadata, + excludeModel as baseExcludeModel } from './baseModelApi.js'; // Load more checkpoints with pagination @@ -85,4 +86,13 @@ export async function saveModelMetadata(filePath, data) { } return response.json(); +} + +/** + * Exclude a checkpoint model from being shown in the UI + * @param {string} filePath - File path of the checkpoint to exclude + * @returns {Promise} Promise resolving to success status + */ +export function excludeCheckpoint(filePath) { + return baseExcludeModel(filePath, 'checkpoint'); } \ No newline at end of file diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 8c4cb9a5..80fa3fb7 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -6,7 +6,8 @@ import { deleteModel as baseDeleteModel, replaceModelPreview, fetchCivitaiMetadata, - refreshSingleModelMetadata + refreshSingleModelMetadata, + excludeModel as baseExcludeModel } from './baseModelApi.js'; /** @@ -34,6 +35,15 @@ export async function saveModelMetadata(filePath, data) { return response.json(); } +/** + * Exclude a lora model from being shown in the UI + * @param {string} filePath - File path of the model to exclude + * @returns {Promise} Promise resolving to success status + */ +export async function excludeLora(filePath) { + return baseExcludeModel(filePath, 'lora'); +} + export async function loadMoreLoras(resetPage = false, updateFolders = false) { return loadMoreModels({ resetPage, diff --git a/static/js/checkpoints.js b/static/js/checkpoints.js index 14036ff3..777277f2 100644 --- a/static/js/checkpoints.js +++ b/static/js/checkpoints.js @@ -1,6 +1,6 @@ import { appCore } from './core.js'; import { initializeInfiniteScroll } from './utils/infiniteScroll.js'; -import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; +import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js'; import { createPageControls } from './components/controls/index.js'; import { loadMoreCheckpoints } from './api/checkpointApi.js'; import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js'; @@ -23,6 +23,8 @@ class CheckpointsPageManager { // Minimal set of functions that need to remain global window.confirmDelete = confirmDelete; window.closeDeleteModal = closeDeleteModal; + window.confirmExclude = confirmExclude; + window.closeExcludeModal = closeExcludeModal; // Add loadCheckpoints function to window for FilterManager compatibility window.checkpointManager = { diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js index a07aee43..0e753fea 100644 --- a/static/js/components/CheckpointCard.js +++ b/static/js/components/CheckpointCard.js @@ -2,7 +2,7 @@ import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showCheckpointModal } from './checkpointModal/index.js'; import { NSFW_LEVELS } from '../utils/constants.js'; -import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata } from '../api/checkpointApi.js'; +import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata, deleteCheckpoint } from '../api/checkpointApi.js'; export function createCheckpointCard(checkpoint) { const card = document.createElement('div'); @@ -322,17 +322,6 @@ function openCivitai(modelName) { } } -function deleteCheckpoint(filePath) { - if (window.deleteCheckpoint) { - window.deleteCheckpoint(filePath); - } else { - // Use the modal delete functionality - import('../utils/modalUtils.js').then(({ showDeleteModal }) => { - showDeleteModal(filePath, 'checkpoint'); - }); - } -} - function replaceCheckpointPreview(filePath) { if (window.replaceCheckpointPreview) { window.replaceCheckpointPreview(filePath); diff --git a/static/js/components/ContextMenu/CheckpointContextMenu.js b/static/js/components/ContextMenu/CheckpointContextMenu.js index c0975bbe..e5e5aea0 100644 --- a/static/js/components/ContextMenu/CheckpointContextMenu.js +++ b/static/js/components/ContextMenu/CheckpointContextMenu.js @@ -3,6 +3,7 @@ import { refreshSingleCheckpointMetadata, saveModelMetadata } from '../../api/ch import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; import { getStorageItem } from '../../utils/storageHelpers.js'; +import { showExcludeModal } from '../../utils/modalUtils.js'; export class CheckpointContextMenu extends BaseContextMenu { constructor() { @@ -61,6 +62,10 @@ export class CheckpointContextMenu extends BaseContextMenu { // Move to folder (placeholder) showToast('Move to folder feature coming soon', 'info'); break; + case 'exclude': + showExcludeModal(this.currentCard.dataset.filepath, 'checkpoint'); + break; + } } diff --git a/static/js/components/ContextMenu/LoraContextMenu.js b/static/js/components/ContextMenu/LoraContextMenu.js index 146c3d94..9d72f99f 100644 --- a/static/js/components/ContextMenu/LoraContextMenu.js +++ b/static/js/components/ContextMenu/LoraContextMenu.js @@ -3,6 +3,7 @@ import { refreshSingleLoraMetadata, saveModelMetadata } from '../../api/loraApi. import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; import { getStorageItem } from '../../utils/storageHelpers.js'; +import { showExcludeModal } from '../../utils/modalUtils.js'; export class LoraContextMenu extends BaseContextMenu { constructor() { @@ -51,6 +52,9 @@ export class LoraContextMenu extends BaseContextMenu { case 'set-nsfw': this.showNSFWLevelSelector(null, null, this.currentCard); break; + case 'exclude': + showExcludeModal(this.currentCard.dataset.filepath); + break; } } diff --git a/static/js/loras.js b/static/js/loras.js index 96f0af37..5dcc9638 100644 --- a/static/js/loras.js +++ b/static/js/loras.js @@ -8,7 +8,7 @@ import { DownloadManager } from './managers/DownloadManager.js'; import { moveManager } from './managers/MoveManager.js'; import { LoraContextMenu } from './components/ContextMenu/index.js'; import { createPageControls } from './components/controls/index.js'; -import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; +import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js'; // Initialize the LoRA page class LoraPageManager { @@ -35,6 +35,8 @@ class LoraPageManager { window.showLoraModal = showLoraModal; window.confirmDelete = confirmDelete; window.closeDeleteModal = closeDeleteModal; + window.confirmExclude = confirmExclude; + window.closeExcludeModal = closeExcludeModal; window.downloadManager = this.downloadManager; window.moveManager = moveManager; window.toggleShowcase = toggleShowcase; diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js index 989a2806..8b1d5b6d 100644 --- a/static/js/managers/ModalManager.js +++ b/static/js/managers/ModalManager.js @@ -59,6 +59,19 @@ export class ModalManager { } }); } + + // Add excludeModal registration + const excludeModal = document.getElementById('excludeModal'); + if (excludeModal) { + this.registerModal('excludeModal', { + element: excludeModal, + onClose: () => { + this.getModal('excludeModal').element.classList.remove('show'); + document.body.classList.remove('modal-open'); + }, + closeOnOutsideClick: true + }); + } // Add downloadModal registration const downloadModal = document.getElementById('downloadModal'); @@ -208,7 +221,7 @@ export class ModalManager { // Store current scroll position before showing modal this.scrollPosition = window.scrollY; - if (id === 'deleteModal') { + if (id === 'deleteModal' || id === 'excludeModal') { modal.element.classList.add('show'); } else { modal.element.style.display = 'block'; diff --git a/static/js/utils/modalUtils.js b/static/js/utils/modalUtils.js index be5fe25d..414eceb0 100644 --- a/static/js/utils/modalUtils.js +++ b/static/js/utils/modalUtils.js @@ -1,15 +1,18 @@ import { modalManager } from '../managers/ModalManager.js'; +import { excludeLora } from '../api/loraApi.js'; +import { excludeCheckpoint } from '../api/checkpointApi.js'; let pendingDeletePath = null; let pendingModelType = null; +let pendingExcludePath = null; +let pendingExcludeModelType = null; export function showDeleteModal(filePath, modelType = 'lora') { - // event.stopPropagation(); pendingDeletePath = filePath; pendingModelType = modelType; const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - const modelName = card.dataset.name; + const modelName = card ? card.dataset.name : filePath.split('/').pop(); const modal = modalManager.getModal('deleteModal').element; const modelInfo = modal.querySelector('.delete-model-info'); @@ -61,4 +64,46 @@ export function closeDeleteModal() { modalManager.closeModal('deleteModal'); pendingDeletePath = null; pendingModelType = null; +} + +// Functions for the exclude modal +export function showExcludeModal(filePath, modelType = 'lora') { + pendingExcludePath = filePath; + pendingExcludeModelType = modelType; + + const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); + const modelName = card ? card.dataset.name : filePath.split('/').pop(); + const modal = modalManager.getModal('excludeModal').element; + const modelInfo = modal.querySelector('.exclude-model-info'); + + modelInfo.innerHTML = ` + Model: ${modelName} +
+ File: ${filePath} + `; + + modalManager.showModal('excludeModal'); +} + +export function closeExcludeModal() { + modalManager.closeModal('excludeModal'); + pendingExcludePath = null; + pendingExcludeModelType = null; +} + +export async function confirmExclude() { + if (!pendingExcludePath) return; + + try { + // Use appropriate exclude function based on model type + if (pendingExcludeModelType === 'checkpoint') { + await excludeCheckpoint(pendingExcludePath); + } else { + await excludeLora(pendingExcludePath); + } + + closeExcludeModal(); + } catch (error) { + console.error('Error excluding model:', error); + } } \ No newline at end of file diff --git a/templates/checkpoints.html b/templates/checkpoints.html index b14be28f..5eb3371d 100644 --- a/templates/checkpoints.html +++ b/templates/checkpoints.html @@ -23,6 +23,7 @@
Set Content Rating
Move to Folder
+
Exclude Model
Delete Model
{% endblock %} diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index 89c19753..b58479b9 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -21,6 +21,9 @@
Move to Folder
+
+ Exclude Model +
Delete Model
diff --git a/templates/components/modals.html b/templates/components/modals.html index 0e3c4ee1..459dc30e 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -11,6 +11,19 @@ + + +