mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
refactor: Consolidate model handling functions into baseModelApi for better code reuse and organization
This commit is contained in:
512
static/js/api/baseModelApi.js
Normal file
512
static/js/api/baseModelApi.js
Normal file
@@ -0,0 +1,512 @@
|
||||
// filepath: d:\Workspace\ComfyUI\custom_nodes\ComfyUI-Lora-Manager\static\js\api\baseModelApi.js
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { showDeleteModal, confirmDelete } from '../utils/modalUtils.js';
|
||||
|
||||
/**
|
||||
* Shared functionality for handling models (loras and checkpoints)
|
||||
*/
|
||||
|
||||
// Generic function to load more models with pagination
|
||||
export async function loadMoreModels(options = {}) {
|
||||
const {
|
||||
resetPage = false,
|
||||
updateFolders = false,
|
||||
modelType = 'lora', // 'lora' or 'checkpoint'
|
||||
createCardFunction,
|
||||
endpoint = '/api/loras'
|
||||
} = options;
|
||||
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
if (pageState.isLoading || (!pageState.hasMore && !resetPage)) return;
|
||||
|
||||
pageState.isLoading = true;
|
||||
document.body.classList.add('loading');
|
||||
|
||||
try {
|
||||
// Reset to first page if requested
|
||||
if (resetPage) {
|
||||
pageState.currentPage = 1;
|
||||
// Clear grid if resetting
|
||||
const gridId = modelType === 'checkpoint' ? 'checkpointGrid' : 'loraGrid';
|
||||
const grid = document.getElementById(gridId);
|
||||
if (grid) grid.innerHTML = '';
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: pageState.currentPage,
|
||||
page_size: pageState.pageSize || 20,
|
||||
sort_by: pageState.sortBy
|
||||
});
|
||||
|
||||
if (pageState.activeFolder !== null) {
|
||||
params.append('folder', pageState.activeFolder);
|
||||
}
|
||||
|
||||
// Add search parameters if there's a search term
|
||||
if (pageState.filters?.search) {
|
||||
params.append('search', pageState.filters.search);
|
||||
params.append('fuzzy', 'true');
|
||||
|
||||
// Add search option parameters if available
|
||||
if (pageState.searchOptions) {
|
||||
params.append('search_filename', pageState.searchOptions.filename.toString());
|
||||
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
||||
if (pageState.searchOptions.tags !== undefined) {
|
||||
params.append('search_tags', pageState.searchOptions.tags.toString());
|
||||
}
|
||||
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Add filter parameters if active
|
||||
if (pageState.filters) {
|
||||
// Handle tags filters
|
||||
if (pageState.filters.tags && pageState.filters.tags.length > 0) {
|
||||
// Checkpoints API expects individual 'tag' parameters, Loras API expects comma-separated 'tags'
|
||||
if (modelType === 'checkpoint') {
|
||||
pageState.filters.tags.forEach(tag => {
|
||||
params.append('tag', tag);
|
||||
});
|
||||
} else {
|
||||
params.append('tags', pageState.filters.tags.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle base model filters
|
||||
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
||||
if (modelType === 'checkpoint') {
|
||||
pageState.filters.baseModel.forEach(model => {
|
||||
params.append('base_model', model);
|
||||
});
|
||||
} else {
|
||||
params.append('base_models', pageState.filters.baseModel.join(','));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add model-specific parameters
|
||||
if (modelType === 'lora') {
|
||||
// Check for recipe-based filtering parameters from session storage
|
||||
const filterLoraHash = getSessionItem ? getSessionItem('recipe_to_lora_filterLoraHash') : null;
|
||||
const filterLoraHashes = getSessionItem ? getSessionItem('recipe_to_lora_filterLoraHashes') : null;
|
||||
|
||||
// Add hash filter parameter if present
|
||||
if (filterLoraHash) {
|
||||
params.append('lora_hash', filterLoraHash);
|
||||
}
|
||||
// Add multiple hashes filter if present
|
||||
else if (filterLoraHashes) {
|
||||
try {
|
||||
if (Array.isArray(filterLoraHashes) && filterLoraHashes.length > 0) {
|
||||
params.append('lora_hashes', filterLoraHashes.join(','));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing lora hashes from session storage:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${endpoint}?${params}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const gridId = modelType === 'checkpoint' ? 'checkpointGrid' : 'loraGrid';
|
||||
const grid = document.getElementById(gridId);
|
||||
|
||||
if (data.items.length === 0 && pageState.currentPage === 1) {
|
||||
grid.innerHTML = `<div class="no-results">No ${modelType}s found in this folder</div>`;
|
||||
pageState.hasMore = false;
|
||||
} else if (data.items.length > 0) {
|
||||
pageState.hasMore = pageState.currentPage < data.total_pages;
|
||||
|
||||
// Append model cards using the provided card creation function
|
||||
data.items.forEach(model => {
|
||||
const card = createCardFunction(model);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Increment the page number AFTER successful loading
|
||||
pageState.currentPage++;
|
||||
} else {
|
||||
pageState.hasMore = false;
|
||||
}
|
||||
|
||||
if (updateFolders && data.folders) {
|
||||
updateFolderTags(data.folders);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error loading ${modelType}s:`, error);
|
||||
showToast(`Failed to load ${modelType}s: ${error.message}`, 'error');
|
||||
} finally {
|
||||
pageState.isLoading = false;
|
||||
document.body.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
// Update folder tags in the UI
|
||||
export function updateFolderTags(folders) {
|
||||
const folderTagsContainer = document.querySelector('.folder-tags');
|
||||
if (!folderTagsContainer) return;
|
||||
|
||||
// Keep track of currently selected folder
|
||||
const pageState = getCurrentPageState();
|
||||
const currentFolder = pageState.activeFolder;
|
||||
|
||||
// Create HTML for folder tags
|
||||
const tagsHTML = folders.map(folder => {
|
||||
const isActive = folder === currentFolder;
|
||||
return `<div class="tag ${isActive ? 'active' : ''}" data-folder="${folder}">${folder}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Update the container
|
||||
folderTagsContainer.innerHTML = tagsHTML;
|
||||
|
||||
// Reattach click handlers and ensure the active tag is visible
|
||||
const tags = folderTagsContainer.querySelectorAll('.tag');
|
||||
tags.forEach(tag => {
|
||||
if (typeof toggleFolder === 'function') {
|
||||
tag.addEventListener('click', toggleFolder);
|
||||
}
|
||||
if (tag.dataset.folder === currentFolder) {
|
||||
tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Generic function to replace a model preview
|
||||
export function replaceModelPreview(filePath, modelType = 'lora') {
|
||||
// Open file picker
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept ='image/*,video/mp4';
|
||||
|
||||
input.onchange = async function() {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
|
||||
const file = input.files[0];
|
||||
await uploadPreview(filePath, file, modelType);
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset and reload models
|
||||
export async function resetAndReload(options = {}) {
|
||||
const {
|
||||
updateFolders = false,
|
||||
modelType = 'lora',
|
||||
loadMoreFunction
|
||||
} = options;
|
||||
|
||||
const pageState = getCurrentPageState();
|
||||
console.log('Resetting with state:', { ...pageState });
|
||||
|
||||
// Reset pagination and load more models
|
||||
if (typeof loadMoreFunction === 'function') {
|
||||
await loadMoreFunction(true, updateFolders);
|
||||
}
|
||||
}
|
||||
|
||||
// Generic function to refresh models
|
||||
export async function refreshModels(options = {}) {
|
||||
const {
|
||||
modelType = 'lora',
|
||||
scanEndpoint = '/api/loras/scan',
|
||||
resetAndReloadFunction
|
||||
} = options;
|
||||
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading(`Refreshing ${modelType}s...`);
|
||||
|
||||
const response = await fetch(scanEndpoint);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to refresh ${modelType}s: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (typeof resetAndReloadFunction === 'function') {
|
||||
await resetAndReloadFunction();
|
||||
}
|
||||
|
||||
showToast(`Refresh complete`, 'success');
|
||||
} catch (error) {
|
||||
console.error(`Refresh failed:`, error);
|
||||
showToast(`Failed to refresh ${modelType}s`, 'error');
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
state.loadingManager.restoreProgressBar();
|
||||
}
|
||||
}
|
||||
|
||||
// Generic fetch from Civitai
|
||||
export async function fetchCivitaiMetadata(options = {}) {
|
||||
const {
|
||||
modelType = 'lora',
|
||||
fetchEndpoint = '/api/fetch-all-civitai',
|
||||
resetAndReloadFunction
|
||||
} = options;
|
||||
|
||||
let ws = null;
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch(data.status) {
|
||||
case 'started':
|
||||
loading.setStatus('Starting metadata fetch...');
|
||||
break;
|
||||
|
||||
case 'processing':
|
||||
const percent = ((data.processed / data.total) * 100).toFixed(1);
|
||||
loading.setProgress(percent);
|
||||
loading.setStatus(
|
||||
`Processing (${data.processed}/${data.total}) ${data.current_name}`
|
||||
);
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
`Completed: Updated ${data.success} of ${data.processed} ${modelType}s`
|
||||
);
|
||||
resolve();
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
reject(new Error('WebSocket error: ' + error.message));
|
||||
};
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = reject;
|
||||
});
|
||||
|
||||
const requestBody = modelType === 'checkpoint'
|
||||
? JSON.stringify({ model_type: 'checkpoint' })
|
||||
: JSON.stringify({});
|
||||
|
||||
const response = await fetch(fetchEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch metadata');
|
||||
}
|
||||
|
||||
await operationComplete;
|
||||
|
||||
if (typeof resetAndReloadFunction === 'function') {
|
||||
await resetAndReloadFunction();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching metadata:', error);
|
||||
showToast('Failed to fetch metadata: ' + error.message, 'error');
|
||||
} finally {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialMessage: 'Connecting...',
|
||||
completionMessage: 'Metadata update complete'
|
||||
});
|
||||
}
|
||||
|
||||
// Generic function to refresh single model metadata
|
||||
export async function refreshSingleModelMetadata(filePath, modelType = 'lora') {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Refreshing metadata...');
|
||||
|
||||
const endpoint = modelType === 'checkpoint'
|
||||
? '/api/checkpoints/fetch-civitai'
|
||||
: '/api/fetch-civitai';
|
||||
|
||||
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 refresh metadata');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('Metadata refreshed successfully', 'success');
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to refresh metadata');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing metadata:', error);
|
||||
showToast(error.message, 'error');
|
||||
return false;
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
state.loadingManager.restoreProgressBar();
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
// Upload a preview image
|
||||
async function uploadPreview(filePath, file, modelType = 'lora') {
|
||||
const loadingOverlay = document.getElementById('loading-overlay');
|
||||
const loadingStatus = document.querySelector('.loading-status');
|
||||
|
||||
try {
|
||||
if (loadingOverlay) loadingOverlay.style.display = 'flex';
|
||||
if (loadingStatus) loadingStatus.textContent = 'Uploading preview...';
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Use appropriate parameter names and endpoint based on model type
|
||||
// Prepare common form data
|
||||
formData.append('preview_file', file);
|
||||
formData.append('model_path', filePath);
|
||||
|
||||
// Set endpoint based on model type
|
||||
const endpoint = modelType === 'checkpoint'
|
||||
? '/api/checkpoints/replace-preview'
|
||||
: '/api/replace_preview';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Update the card preview in UI
|
||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
if (card) {
|
||||
const previewContainer = card.querySelector('.card-preview');
|
||||
const oldPreview = previewContainer.querySelector('img, video');
|
||||
|
||||
// For LoRA models, use timestamp to prevent caching
|
||||
if (modelType === 'lora') {
|
||||
state.previewVersions?.set(filePath, Date.now());
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const previewUrl = data.preview_url ?
|
||||
`${data.preview_url}?t=${timestamp}` :
|
||||
`/api/model/preview_image?path=${encodeURIComponent(filePath)}&t=${timestamp}`;
|
||||
|
||||
// Create appropriate element based on file type
|
||||
if (file.type.startsWith('video/')) {
|
||||
const video = document.createElement('video');
|
||||
video.controls = true;
|
||||
video.autoplay = true;
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.src = previewUrl;
|
||||
oldPreview.replaceWith(video);
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
oldPreview.replaceWith(img);
|
||||
}
|
||||
|
||||
showToast('Preview updated successfully', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading preview:', error);
|
||||
showToast('Failed to upload preview image', 'error');
|
||||
} finally {
|
||||
if (loadingOverlay) loadingOverlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Private function to perform the delete operation
|
||||
async function performDelete(filePath, modelType = 'lora') {
|
||||
try {
|
||||
showToast(`Deleting ${modelType}...`, 'info');
|
||||
|
||||
const response = await fetch('/api/model/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
model_type: modelType
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete ${modelType}: ${response.status} ${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} deleted successfully`, 'success');
|
||||
} else {
|
||||
throw new Error(data.error || `Failed to delete ${modelType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error deleting ${modelType}:`, error);
|
||||
showToast(`Failed to delete ${modelType}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get session item - import if available, otherwise provide fallback
|
||||
function getSessionItem(key) {
|
||||
if (typeof window !== 'undefined' && window.sessionStorage) {
|
||||
const item = window.sessionStorage.getItem(key);
|
||||
try {
|
||||
return item ? JSON.parse(item) : null;
|
||||
} catch (e) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,330 +1,57 @@
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { confirmDelete } from '../utils/modalUtils.js';
|
||||
import { createCheckpointCard } from '../components/CheckpointCard.js';
|
||||
import {
|
||||
loadMoreModels,
|
||||
resetAndReload as baseResetAndReload,
|
||||
refreshModels as baseRefreshModels,
|
||||
deleteModel as baseDeleteModel,
|
||||
replaceModelPreview,
|
||||
fetchCivitaiMetadata
|
||||
} from './baseModelApi.js';
|
||||
|
||||
// Load more checkpoints with pagination
|
||||
export async function loadMoreCheckpoints(resetPagination = true) {
|
||||
try {
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
// Don't load if we're already loading or there are no more items
|
||||
if (pageState.isLoading || (!resetPagination && !pageState.hasMore)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set loading state
|
||||
pageState.isLoading = true;
|
||||
document.body.classList.add('loading');
|
||||
|
||||
// Reset pagination if requested
|
||||
if (resetPagination) {
|
||||
pageState.currentPage = 1;
|
||||
const grid = document.getElementById('checkpointGrid');
|
||||
if (grid) grid.innerHTML = '';
|
||||
}
|
||||
|
||||
// Build API URL with parameters
|
||||
const params = new URLSearchParams({
|
||||
page: pageState.currentPage,
|
||||
page_size: pageState.pageSize || 20,
|
||||
sort: pageState.sortBy || 'name'
|
||||
});
|
||||
|
||||
// Add folder filter if active
|
||||
if (pageState.activeFolder) {
|
||||
params.append('folder', pageState.activeFolder);
|
||||
}
|
||||
|
||||
// Add search if available
|
||||
if (pageState.filters && pageState.filters.search) {
|
||||
params.append('search', pageState.filters.search);
|
||||
|
||||
// Add search options
|
||||
if (pageState.searchOptions) {
|
||||
params.append('search_filename', pageState.searchOptions.filename.toString());
|
||||
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
||||
params.append('recursive', pageState.searchOptions.recursive.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Add base model filters
|
||||
if (pageState.filters && pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
||||
pageState.filters.baseModel.forEach(model => {
|
||||
params.append('base_model', model);
|
||||
});
|
||||
}
|
||||
|
||||
// Add tags filters
|
||||
if (pageState.filters && pageState.filters.tags && pageState.filters.tags.length > 0) {
|
||||
pageState.filters.tags.forEach(tag => {
|
||||
params.append('tag', tag);
|
||||
});
|
||||
}
|
||||
|
||||
// Execute fetch
|
||||
const response = await fetch(`/api/checkpoints?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load checkpoints: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Update state with response data
|
||||
pageState.hasMore = data.page < data.total_pages;
|
||||
|
||||
// Update UI with checkpoints
|
||||
const grid = document.getElementById('checkpointGrid');
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear grid if this is the first page
|
||||
if (resetPagination) {
|
||||
grid.innerHTML = '';
|
||||
}
|
||||
|
||||
// Check for empty result
|
||||
if (data.items.length === 0 && resetPagination) {
|
||||
grid.innerHTML = `
|
||||
<div class="placeholder-message">
|
||||
<p>No checkpoints found</p>
|
||||
<p>Add checkpoints to your models folders to see them here.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Render checkpoint cards
|
||||
data.items.forEach(checkpoint => {
|
||||
const card = createCheckpointCard(checkpoint);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Increment the page number AFTER successful loading
|
||||
if (data.items.length > 0) {
|
||||
pageState.currentPage++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading checkpoints:', error);
|
||||
showToast('Failed to load checkpoints', 'error');
|
||||
} finally {
|
||||
// Clear loading state
|
||||
const pageState = getCurrentPageState();
|
||||
pageState.isLoading = false;
|
||||
document.body.classList.remove('loading');
|
||||
}
|
||||
return loadMoreModels({
|
||||
resetPage: resetPagination,
|
||||
updateFolders: true,
|
||||
modelType: 'checkpoint',
|
||||
createCardFunction: createCheckpointCard,
|
||||
endpoint: '/api/checkpoints'
|
||||
});
|
||||
}
|
||||
|
||||
// Reset and reload checkpoints
|
||||
export async function resetAndReload() {
|
||||
const pageState = getCurrentPageState();
|
||||
pageState.currentPage = 1;
|
||||
pageState.hasMore = true;
|
||||
await loadMoreCheckpoints(true);
|
||||
return baseResetAndReload({
|
||||
updateFolders: true,
|
||||
modelType: 'checkpoint',
|
||||
loadMoreFunction: loadMoreCheckpoints
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh checkpoints
|
||||
export async function refreshCheckpoints() {
|
||||
try {
|
||||
showToast('Scanning for checkpoints...', 'info');
|
||||
const response = await fetch('/api/checkpoints/scan');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to scan checkpoints: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
await resetAndReload();
|
||||
showToast('Checkpoints refreshed successfully', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error refreshing checkpoints:', error);
|
||||
showToast('Failed to refresh checkpoints', 'error');
|
||||
}
|
||||
return baseRefreshModels({
|
||||
modelType: 'checkpoint',
|
||||
scanEndpoint: '/api/checkpoints/scan',
|
||||
resetAndReloadFunction: resetAndReload
|
||||
});
|
||||
}
|
||||
|
||||
// Delete a checkpoint
|
||||
export function deleteCheckpoint(filePath) {
|
||||
confirmDelete('Are you sure you want to delete this checkpoint?', () => {
|
||||
_performDelete(filePath);
|
||||
});
|
||||
}
|
||||
|
||||
// Private function to perform the delete operation
|
||||
async function _performDelete(filePath) {
|
||||
try {
|
||||
showToast('Deleting checkpoint...', 'info');
|
||||
|
||||
const response = await fetch('/api/model/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
model_type: 'checkpoint'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete checkpoint: ${response.status} ${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('Checkpoint deleted successfully', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to delete checkpoint');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting checkpoint:', error);
|
||||
showToast(`Failed to delete checkpoint: ${error.message}`, 'error');
|
||||
}
|
||||
return baseDeleteModel(filePath, 'checkpoint');
|
||||
}
|
||||
|
||||
// Replace checkpoint preview
|
||||
export function replaceCheckpointPreview(filePath) {
|
||||
// Open file picker
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = async (e) => {
|
||||
if (!e.target.files.length) return;
|
||||
|
||||
const file = e.target.files[0];
|
||||
await _uploadPreview(filePath, file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
// Upload a preview image
|
||||
async function _uploadPreview(filePath, file) {
|
||||
try {
|
||||
showToast('Uploading preview...', 'info');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('file_path', filePath);
|
||||
formData.append('model_type', 'checkpoint');
|
||||
|
||||
const response = await fetch('/api/model/preview', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload preview: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Update the preview in UI
|
||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
if (card) {
|
||||
const img = card.querySelector('.card-preview img');
|
||||
if (img) {
|
||||
// Add timestamp to prevent caching
|
||||
const timestamp = new Date().getTime();
|
||||
if (data.preview_url) {
|
||||
img.src = `${data.preview_url}?t=${timestamp}`;
|
||||
} else {
|
||||
img.src = `/api/model/preview_image?path=${encodeURIComponent(filePath)}&t=${timestamp}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showToast('Preview updated successfully', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to update preview');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating preview:', error);
|
||||
showToast(`Failed to update preview: ${error.message}`, 'error');
|
||||
}
|
||||
return replaceModelPreview(filePath, 'checkpoint');
|
||||
}
|
||||
|
||||
// Fetch metadata from Civitai for checkpoints
|
||||
export async function fetchCivitai() {
|
||||
let ws = null;
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch(data.status) {
|
||||
case 'started':
|
||||
loading.setStatus('Starting metadata fetch...');
|
||||
break;
|
||||
|
||||
case 'processing':
|
||||
const percent = ((data.processed / data.total) * 100).toFixed(1);
|
||||
loading.setProgress(percent);
|
||||
loading.setStatus(
|
||||
`Processing (${data.processed}/${data.total}) ${data.current_name}`
|
||||
);
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
`Completed: Updated ${data.success} of ${data.processed} checkpoints`
|
||||
);
|
||||
resolve();
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
reject(new Error('WebSocket error: ' + error.message));
|
||||
};
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = reject;
|
||||
});
|
||||
|
||||
const response = await fetch('/api/checkpoints/fetch-all-civitai', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model_type: 'checkpoint' }) // Specify we're fetching checkpoint metadata
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch metadata');
|
||||
}
|
||||
|
||||
await operationComplete;
|
||||
|
||||
await resetAndReload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching metadata:', error);
|
||||
showToast('Failed to fetch metadata: ' + error.message, 'error');
|
||||
} finally {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialMessage: 'Connecting...',
|
||||
completionMessage: 'Metadata update complete'
|
||||
return fetchCivitaiMetadata({
|
||||
modelType: 'checkpoint',
|
||||
fetchEndpoint: '/api/checkpoints/fetch-all-civitai',
|
||||
resetAndReloadFunction: resetAndReload
|
||||
});
|
||||
}
|
||||
@@ -1,285 +1,38 @@
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { createLoraCard } from '../components/LoraCard.js';
|
||||
import { initializeInfiniteScroll } from '../utils/infiniteScroll.js';
|
||||
import { showDeleteModal } from '../utils/modalUtils.js';
|
||||
import { toggleFolder } from '../utils/uiHelpers.js';
|
||||
import { getSessionItem } from '../utils/storageHelpers.js';
|
||||
import {
|
||||
loadMoreModels,
|
||||
resetAndReload as baseResetAndReload,
|
||||
refreshModels as baseRefreshModels,
|
||||
deleteModel as baseDeleteModel,
|
||||
replaceModelPreview,
|
||||
fetchCivitaiMetadata,
|
||||
refreshSingleModelMetadata
|
||||
} from './baseModelApi.js';
|
||||
|
||||
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
if (pageState.isLoading || (!pageState.hasMore && !resetPage)) return;
|
||||
|
||||
pageState.isLoading = true;
|
||||
try {
|
||||
// Reset to first page if requested
|
||||
if (resetPage) {
|
||||
pageState.currentPage = 1;
|
||||
// Clear grid if resetting
|
||||
const grid = document.getElementById('loraGrid');
|
||||
if (grid) grid.innerHTML = '';
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: pageState.currentPage,
|
||||
page_size: 20,
|
||||
sort_by: pageState.sortBy
|
||||
});
|
||||
|
||||
if (pageState.activeFolder !== null) {
|
||||
params.append('folder', pageState.activeFolder);
|
||||
}
|
||||
|
||||
// Add search parameters if there's a search term
|
||||
if (pageState.filters?.search) {
|
||||
params.append('search', pageState.filters.search);
|
||||
params.append('fuzzy', 'true');
|
||||
|
||||
// Add search option parameters if available
|
||||
if (pageState.searchOptions) {
|
||||
params.append('search_filename', pageState.searchOptions.filename.toString());
|
||||
params.append('search_modelname', pageState.searchOptions.modelname.toString());
|
||||
params.append('search_tags', (pageState.searchOptions.tags || false).toString());
|
||||
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Add filter parameters if active
|
||||
if (pageState.filters) {
|
||||
if (pageState.filters.tags && pageState.filters.tags.length > 0) {
|
||||
// Convert the array of tags to a comma-separated string
|
||||
params.append('tags', pageState.filters.tags.join(','));
|
||||
}
|
||||
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
||||
// Convert the array of base models to a comma-separated string
|
||||
params.append('base_models', pageState.filters.baseModel.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for recipe-based filtering parameters from session storage
|
||||
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
|
||||
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
|
||||
|
||||
// Add hash filter parameter if present
|
||||
if (filterLoraHash) {
|
||||
params.append('lora_hash', filterLoraHash);
|
||||
}
|
||||
// Add multiple hashes filter if present
|
||||
else if (filterLoraHashes) {
|
||||
try {
|
||||
if (Array.isArray(filterLoraHashes) && filterLoraHashes.length > 0) {
|
||||
params.append('lora_hashes', filterLoraHashes.join(','));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing lora hashes from session storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/loras?${params}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch loras: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.items.length === 0 && pageState.currentPage === 1) {
|
||||
const grid = document.getElementById('loraGrid');
|
||||
grid.innerHTML = '<div class="no-results">No loras found in this folder</div>';
|
||||
pageState.hasMore = false;
|
||||
} else if (data.items.length > 0) {
|
||||
pageState.hasMore = pageState.currentPage < data.total_pages;
|
||||
appendLoraCards(data.items);
|
||||
|
||||
// Increment the page number AFTER successful loading
|
||||
pageState.currentPage++;
|
||||
} else {
|
||||
pageState.hasMore = false;
|
||||
}
|
||||
|
||||
if (updateFolders && data.folders) {
|
||||
updateFolderTags(data.folders);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading loras:', error);
|
||||
showToast('Failed to load loras: ' + error.message, 'error');
|
||||
} finally {
|
||||
pageState.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFolderTags(folders) {
|
||||
const folderTagsContainer = document.querySelector('.folder-tags');
|
||||
if (!folderTagsContainer) return;
|
||||
|
||||
// Keep track of currently selected folder
|
||||
const pageState = getCurrentPageState();
|
||||
const currentFolder = pageState.activeFolder;
|
||||
|
||||
// Create HTML for folder tags
|
||||
const tagsHTML = folders.map(folder => {
|
||||
const isActive = folder === currentFolder;
|
||||
return `<div class="tag ${isActive ? 'active' : ''}" data-folder="${folder}">${folder}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Update the container
|
||||
folderTagsContainer.innerHTML = tagsHTML;
|
||||
|
||||
// Reattach click handlers and ensure the active tag is visible
|
||||
const tags = folderTagsContainer.querySelectorAll('.tag');
|
||||
tags.forEach(tag => {
|
||||
tag.addEventListener('click', toggleFolder);
|
||||
if (tag.dataset.folder === currentFolder) {
|
||||
tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
return loadMoreModels({
|
||||
resetPage,
|
||||
updateFolders,
|
||||
modelType: 'lora',
|
||||
createCardFunction: createLoraCard,
|
||||
endpoint: '/api/loras'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchCivitai() {
|
||||
let ws = null;
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch(data.status) {
|
||||
case 'started':
|
||||
loading.setStatus('Starting metadata fetch...');
|
||||
break;
|
||||
|
||||
case 'processing':
|
||||
const percent = ((data.processed / data.total) * 100).toFixed(1);
|
||||
loading.setProgress(percent);
|
||||
loading.setStatus(
|
||||
`Processing (${data.processed}/${data.total}) ${data.current_name}`
|
||||
);
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
`Completed: Updated ${data.success} of ${data.processed} loras`
|
||||
);
|
||||
resolve();
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
reject(new Error('WebSocket error: ' + error.message));
|
||||
};
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = reject;
|
||||
});
|
||||
|
||||
const response = await fetch('/api/fetch-all-civitai', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch metadata');
|
||||
}
|
||||
|
||||
await operationComplete;
|
||||
|
||||
await resetAndReload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching metadata:', error);
|
||||
showToast('Failed to fetch metadata: ' + error.message, 'error');
|
||||
} finally {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialMessage: 'Connecting...',
|
||||
completionMessage: 'Metadata update complete'
|
||||
return fetchCivitaiMetadata({
|
||||
modelType: 'lora',
|
||||
fetchEndpoint: '/api/fetch-all-civitai',
|
||||
resetAndReloadFunction: resetAndReload
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteModel(filePath) {
|
||||
showDeleteModal(filePath);
|
||||
return baseDeleteModel(filePath, 'lora');
|
||||
}
|
||||
|
||||
export async function replacePreview(filePath) {
|
||||
const loadingOverlay = document.getElementById('loading-overlay');
|
||||
const loadingStatus = document.querySelector('.loading-status');
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*,video/mp4';
|
||||
|
||||
input.onchange = async function() {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
|
||||
const file = input.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('preview_file', file);
|
||||
formData.append('model_path', filePath);
|
||||
|
||||
try {
|
||||
loadingOverlay.style.display = 'flex';
|
||||
loadingStatus.textContent = 'Uploading preview...';
|
||||
|
||||
const response = await fetch('/api/replace_preview', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 更新预览版本
|
||||
state.previewVersions.set(filePath, Date.now());
|
||||
|
||||
// 更新卡片显示
|
||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
const previewContainer = card.querySelector('.card-preview');
|
||||
const oldPreview = previewContainer.querySelector('img, video');
|
||||
|
||||
const previewUrl = `${data.preview_url}?t=${state.previewVersions.get(filePath)}`;
|
||||
|
||||
if (file.type.startsWith('video/')) {
|
||||
const video = document.createElement('video');
|
||||
video.controls = true;
|
||||
video.autoplay = true;
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.src = previewUrl;
|
||||
oldPreview.replaceWith(video);
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
oldPreview.replaceWith(img);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading preview:', error);
|
||||
alert('Failed to upload preview image');
|
||||
} finally {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
return replaceModelPreview(filePath, 'lora');
|
||||
}
|
||||
|
||||
export function appendLoraCards(loras) {
|
||||
@@ -293,57 +46,26 @@ export function appendLoraCards(loras) {
|
||||
}
|
||||
|
||||
export async function resetAndReload(updateFolders = false) {
|
||||
const pageState = getCurrentPageState();
|
||||
console.log('Resetting with state:', { ...pageState });
|
||||
|
||||
// Reset pagination and load more loras
|
||||
await loadMoreLoras(true, updateFolders);
|
||||
return baseResetAndReload({
|
||||
updateFolders,
|
||||
modelType: 'lora',
|
||||
loadMoreFunction: loadMoreLoras
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshLoras() {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Refreshing loras...');
|
||||
await resetAndReload();
|
||||
showToast('Refresh complete', 'success');
|
||||
} catch (error) {
|
||||
console.error('Refresh failed:', error);
|
||||
showToast('Failed to refresh loras', 'error');
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
state.loadingManager.restoreProgressBar();
|
||||
}
|
||||
return baseRefreshModels({
|
||||
modelType: 'lora',
|
||||
scanEndpoint: '/api/loras/scan',
|
||||
resetAndReloadFunction: resetAndReload
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshSingleLoraMetadata(filePath) {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Refreshing metadata...');
|
||||
const response = await fetch('/api/fetch-civitai', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ file_path: filePath })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to refresh metadata');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast('Metadata refreshed successfully', 'success');
|
||||
// Reload the current view to show updated data
|
||||
await resetAndReload();
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to refresh metadata');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing metadata:', error);
|
||||
showToast(error.message, 'error');
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
state.loadingManager.restoreProgressBar();
|
||||
const success = await refreshSingleModelMetadata(filePath, 'lora');
|
||||
if (success) {
|
||||
// Reload the current view to show updated data
|
||||
await resetAndReload();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { showToast } 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 } from '../api/checkpointApi.js';
|
||||
|
||||
export function createCheckpointCard(checkpoint) {
|
||||
const card = document.createElement('div');
|
||||
@@ -305,6 +306,6 @@ function replaceCheckpointPreview(filePath) {
|
||||
if (window.replaceCheckpointPreview) {
|
||||
window.replaceCheckpointPreview(filePath);
|
||||
} else {
|
||||
console.log('Replace checkpoint preview:', filePath);
|
||||
apiReplaceCheckpointPreview(filePath);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user