feat: add model exclution functionality frontend

This commit is contained in:
Will Miao
2025-05-03 15:43:04 +08:00
parent ec1c92a714
commit 611dd33c75
15 changed files with 175 additions and 44 deletions

View File

@@ -43,7 +43,7 @@ class ApiRoutes:
app.on_startup.append(lambda _: routes.initialize_services()) app.on_startup.append(lambda _: routes.initialize_services())
app.router.add_post('/api/delete_model', routes.delete_model) 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/fetch-civitai', routes.fetch_civitai)
app.router.add_post('/api/replace_preview', routes.replace_preview) app.router.add_post('/api/replace_preview', routes.replace_preview)
app.router.add_get('/api/loras', routes.get_loras) app.router.add_get('/api/loras', routes.get_loras)

View File

@@ -44,26 +44,12 @@ body.modal-open {
} }
/* Delete Modal specific styles */ /* Delete Modal specific styles */
.delete-modal-content {
max-width: 500px;
text-align: center;
}
.delete-message { .delete-message {
color: var(--text-color); color: var(--text-color);
margin: var(--space-2) 0; 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 */ /* Update delete modal styles */
.delete-modal { .delete-modal {
display: none; /* Set initial display to none */ display: none; /* Set initial display to none */
@@ -92,7 +78,8 @@ body.modal-open {
animation: modalFadeIn 0.2s ease-out; animation: modalFadeIn 0.2s ease-out;
} }
.delete-model-info { .delete-model-info,
.exclude-model-info {
/* Update info display styling */ /* Update info display styling */
background: var(--lora-surface); background: var(--lora-surface);
border: 1px solid var(--lora-border); border: 1px solid var(--lora-border);
@@ -123,7 +110,7 @@ body.modal-open {
margin-top: var(--space-3); margin-top: var(--space-3);
} }
.cancel-btn, .delete-btn { .cancel-btn, .delete-btn, .exclude-btn {
padding: 8px var(--space-2); padding: 8px var(--space-2);
border-radius: 6px; border-radius: 6px;
border: none; border: none;
@@ -143,6 +130,12 @@ body.modal-open {
color: white; color: white;
} }
/* Style for exclude button - different from delete button */
.exclude-btn {
background: var(--lora-accent, #4f46e5);
color: white;
}
.cancel-btn:hover { .cancel-btn:hover {
background: var(--lora-border); background: var(--lora-border);
} }
@@ -151,6 +144,11 @@ body.modal-open {
opacity: 0.9; opacity: 0.9;
} }
.exclude-btn:hover {
opacity: 0.9;
background: oklch(from var(--lora-accent, #4f46e5) l c h / 85%);
}
.modal-content h2 { .modal-content h2 {
color: var(--text-color); color: var(--text-color);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
@@ -587,7 +585,7 @@ input:checked + .toggle-slider:before {
border-radius: var(--border-radius-xs); border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background-color: var(--lora-surface); background-color: var(--lora-surface);
color: var(--text-color); color: var (--text-color);
font-size: 0.95em; font-size: 0.95em;
height: 32px; height: 32px;
} }

View File

@@ -209,13 +209,7 @@ export function replaceModelPreview(filePath, modelType = 'lora') {
// Delete a model (generic) // Delete a model (generic)
export function deleteModel(filePath, modelType = 'lora') { export function deleteModel(filePath, modelType = 'lora') {
if (modelType === 'checkpoint') { showDeleteModal(filePath);
confirmDelete('Are you sure you want to delete this checkpoint?', () => {
performDelete(filePath, modelType);
});
} else {
showDeleteModal(filePath);
}
} }
// Reset and reload models // 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 // Private methods
// Upload a preview image // Upload a preview image

View File

@@ -6,7 +6,8 @@ import {
deleteModel as baseDeleteModel, deleteModel as baseDeleteModel,
replaceModelPreview, replaceModelPreview,
fetchCivitaiMetadata, fetchCivitaiMetadata,
refreshSingleModelMetadata refreshSingleModelMetadata,
excludeModel as baseExcludeModel
} from './baseModelApi.js'; } from './baseModelApi.js';
// Load more checkpoints with pagination // Load more checkpoints with pagination
@@ -85,4 +86,13 @@ export async function saveModelMetadata(filePath, data) {
} }
return response.json(); 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<boolean>} Promise resolving to success status
*/
export function excludeCheckpoint(filePath) {
return baseExcludeModel(filePath, 'checkpoint');
} }

View File

@@ -6,7 +6,8 @@ import {
deleteModel as baseDeleteModel, deleteModel as baseDeleteModel,
replaceModelPreview, replaceModelPreview,
fetchCivitaiMetadata, fetchCivitaiMetadata,
refreshSingleModelMetadata refreshSingleModelMetadata,
excludeModel as baseExcludeModel
} from './baseModelApi.js'; } from './baseModelApi.js';
/** /**
@@ -34,6 +35,15 @@ export async function saveModelMetadata(filePath, data) {
return response.json(); 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<boolean>} Promise resolving to success status
*/
export async function excludeLora(filePath) {
return baseExcludeModel(filePath, 'lora');
}
export async function loadMoreLoras(resetPage = false, updateFolders = false) { export async function loadMoreLoras(resetPage = false, updateFolders = false) {
return loadMoreModels({ return loadMoreModels({
resetPage, resetPage,

View File

@@ -1,6 +1,6 @@
import { appCore } from './core.js'; import { appCore } from './core.js';
import { initializeInfiniteScroll } from './utils/infiniteScroll.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 { createPageControls } from './components/controls/index.js';
import { loadMoreCheckpoints } from './api/checkpointApi.js'; import { loadMoreCheckpoints } from './api/checkpointApi.js';
import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js'; import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js';
@@ -23,6 +23,8 @@ class CheckpointsPageManager {
// Minimal set of functions that need to remain global // Minimal set of functions that need to remain global
window.confirmDelete = confirmDelete; window.confirmDelete = confirmDelete;
window.closeDeleteModal = closeDeleteModal; window.closeDeleteModal = closeDeleteModal;
window.confirmExclude = confirmExclude;
window.closeExcludeModal = closeExcludeModal;
// Add loadCheckpoints function to window for FilterManager compatibility // Add loadCheckpoints function to window for FilterManager compatibility
window.checkpointManager = { window.checkpointManager = {

View File

@@ -2,7 +2,7 @@ import { showToast, copyToClipboard } from '../utils/uiHelpers.js';
import { state } from '../state/index.js'; import { state } from '../state/index.js';
import { showCheckpointModal } from './checkpointModal/index.js'; import { showCheckpointModal } from './checkpointModal/index.js';
import { NSFW_LEVELS } from '../utils/constants.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) { export function createCheckpointCard(checkpoint) {
const card = document.createElement('div'); 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) { function replaceCheckpointPreview(filePath) {
if (window.replaceCheckpointPreview) { if (window.replaceCheckpointPreview) {
window.replaceCheckpointPreview(filePath); window.replaceCheckpointPreview(filePath);

View File

@@ -3,6 +3,7 @@ import { refreshSingleCheckpointMetadata, saveModelMetadata } from '../../api/ch
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js'; import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
import { NSFW_LEVELS } from '../../utils/constants.js'; import { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js'; import { getStorageItem } from '../../utils/storageHelpers.js';
import { showExcludeModal } from '../../utils/modalUtils.js';
export class CheckpointContextMenu extends BaseContextMenu { export class CheckpointContextMenu extends BaseContextMenu {
constructor() { constructor() {
@@ -61,6 +62,10 @@ export class CheckpointContextMenu extends BaseContextMenu {
// Move to folder (placeholder) // Move to folder (placeholder)
showToast('Move to folder feature coming soon', 'info'); showToast('Move to folder feature coming soon', 'info');
break; break;
case 'exclude':
showExcludeModal(this.currentCard.dataset.filepath, 'checkpoint');
break;
} }
} }

View File

@@ -3,6 +3,7 @@ import { refreshSingleLoraMetadata, saveModelMetadata } from '../../api/loraApi.
import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js'; import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
import { NSFW_LEVELS } from '../../utils/constants.js'; import { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js'; import { getStorageItem } from '../../utils/storageHelpers.js';
import { showExcludeModal } from '../../utils/modalUtils.js';
export class LoraContextMenu extends BaseContextMenu { export class LoraContextMenu extends BaseContextMenu {
constructor() { constructor() {
@@ -51,6 +52,9 @@ export class LoraContextMenu extends BaseContextMenu {
case 'set-nsfw': case 'set-nsfw':
this.showNSFWLevelSelector(null, null, this.currentCard); this.showNSFWLevelSelector(null, null, this.currentCard);
break; break;
case 'exclude':
showExcludeModal(this.currentCard.dataset.filepath);
break;
} }
} }

View File

@@ -8,7 +8,7 @@ import { DownloadManager } from './managers/DownloadManager.js';
import { moveManager } from './managers/MoveManager.js'; import { moveManager } from './managers/MoveManager.js';
import { LoraContextMenu } from './components/ContextMenu/index.js'; import { LoraContextMenu } from './components/ContextMenu/index.js';
import { createPageControls } from './components/controls/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 // Initialize the LoRA page
class LoraPageManager { class LoraPageManager {
@@ -35,6 +35,8 @@ class LoraPageManager {
window.showLoraModal = showLoraModal; window.showLoraModal = showLoraModal;
window.confirmDelete = confirmDelete; window.confirmDelete = confirmDelete;
window.closeDeleteModal = closeDeleteModal; window.closeDeleteModal = closeDeleteModal;
window.confirmExclude = confirmExclude;
window.closeExcludeModal = closeExcludeModal;
window.downloadManager = this.downloadManager; window.downloadManager = this.downloadManager;
window.moveManager = moveManager; window.moveManager = moveManager;
window.toggleShowcase = toggleShowcase; window.toggleShowcase = toggleShowcase;

View File

@@ -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 // Add downloadModal registration
const downloadModal = document.getElementById('downloadModal'); const downloadModal = document.getElementById('downloadModal');
@@ -208,7 +221,7 @@ export class ModalManager {
// Store current scroll position before showing modal // Store current scroll position before showing modal
this.scrollPosition = window.scrollY; this.scrollPosition = window.scrollY;
if (id === 'deleteModal') { if (id === 'deleteModal' || id === 'excludeModal') {
modal.element.classList.add('show'); modal.element.classList.add('show');
} else { } else {
modal.element.style.display = 'block'; modal.element.style.display = 'block';

View File

@@ -1,15 +1,18 @@
import { modalManager } from '../managers/ModalManager.js'; import { modalManager } from '../managers/ModalManager.js';
import { excludeLora } from '../api/loraApi.js';
import { excludeCheckpoint } from '../api/checkpointApi.js';
let pendingDeletePath = null; let pendingDeletePath = null;
let pendingModelType = null; let pendingModelType = null;
let pendingExcludePath = null;
let pendingExcludeModelType = null;
export function showDeleteModal(filePath, modelType = 'lora') { export function showDeleteModal(filePath, modelType = 'lora') {
// event.stopPropagation();
pendingDeletePath = filePath; pendingDeletePath = filePath;
pendingModelType = modelType; pendingModelType = modelType;
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); 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 modal = modalManager.getModal('deleteModal').element;
const modelInfo = modal.querySelector('.delete-model-info'); const modelInfo = modal.querySelector('.delete-model-info');
@@ -61,4 +64,46 @@ export function closeDeleteModal() {
modalManager.closeModal('deleteModal'); modalManager.closeModal('deleteModal');
pendingDeletePath = null; pendingDeletePath = null;
pendingModelType = 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 = `
<strong>Model:</strong> ${modelName}
<br>
<strong>File:</strong> ${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);
}
} }

View File

@@ -23,6 +23,7 @@
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> Set Content Rating</div> <div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> Set Content Rating</div>
<div class="context-menu-separator"></div> <div class="context-menu-separator"></div>
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> Move to Folder</div> <div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> Move to Folder</div>
<div class="context-menu-item" data-action="exclude"><i class="fas fa-eye-slash"></i> Exclude Model</div>
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> Delete Model</div> <div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> Delete Model</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -21,6 +21,9 @@
<div class="context-menu-item" data-action="move"> <div class="context-menu-item" data-action="move">
<i class="fas fa-folder-open"></i> Move to Folder <i class="fas fa-folder-open"></i> Move to Folder
</div> </div>
<div class="context-menu-item" data-action="exclude">
<i class="fas fa-eye-slash"></i> Exclude Model
</div>
<div class="context-menu-item delete-item" data-action="delete"> <div class="context-menu-item delete-item" data-action="delete">
<i class="fas fa-trash"></i> Delete Model <i class="fas fa-trash"></i> Delete Model
</div> </div>

View File

@@ -11,6 +11,19 @@
</div> </div>
</div> </div>
<!-- Exclude Confirmation Modal -->
<div id="excludeModal" class="modal delete-modal">
<div class="modal-content delete-modal-content">
<h2>Exclude Model</h2>
<p class="delete-message">Are you sure you want to exclude this model? Excluded models won't appear in searches or model lists.</p>
<div class="exclude-model-info"></div>
<div class="modal-actions">
<button class="cancel-btn" onclick="closeExcludeModal()">Cancel</button>
<button class="exclude-btn" onclick="confirmExclude()">Exclude</button>
</div>
</div>
</div>
<!-- Settings Modal --> <!-- Settings Modal -->
<div id="settingsModal" class="modal"> <div id="settingsModal" class="modal">
<div class="modal-content settings-modal"> <div class="modal-content settings-modal">