mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Refactor API structure to unify model operations
- Introduced MODEL_TYPES and MODEL_CONFIG for centralized model type management. - Created a unified API client for checkpoints and loras to streamline operations. - Updated all API calls in checkpointApi.js and loraApi.js to use the new client. - Simplified context menus and model card operations to leverage the unified API client. - Enhanced state management to accommodate new model types and their configurations. - Added virtual scrolling functions for recipes and improved loading states. - Refactored modal utilities to handle model exclusion and deletion generically. - Improved error handling and user feedback across various operations.
This commit is contained in:
@@ -1038,6 +1038,7 @@ class ModelRouteUtils:
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'new_file_path': new_file_path,
|
||||
'new_preview_path': config.get_preview_static_url(new_preview),
|
||||
'renamed_files': renamed_files,
|
||||
'reload_required': False
|
||||
})
|
||||
|
||||
169
static/js/api/apiConfig.js
Normal file
169
static/js/api/apiConfig.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { state } from '../state/index.js';
|
||||
|
||||
/**
|
||||
* API Configuration
|
||||
* Centralized configuration for all model types and their endpoints
|
||||
*/
|
||||
|
||||
// Model type definitions
|
||||
export const MODEL_TYPES = {
|
||||
LORA: 'loras',
|
||||
CHECKPOINT: 'checkpoints',
|
||||
EMBEDDING: 'embeddings' // Future model type
|
||||
};
|
||||
|
||||
// Base API configuration for each model type
|
||||
export const MODEL_CONFIG = {
|
||||
[MODEL_TYPES.LORA]: {
|
||||
displayName: 'LoRA',
|
||||
singularName: 'lora',
|
||||
defaultPageSize: 100,
|
||||
supportsLetterFilter: true,
|
||||
supportsBulkOperations: true,
|
||||
supportsMove: true,
|
||||
templateName: 'loras.html'
|
||||
},
|
||||
[MODEL_TYPES.CHECKPOINT]: {
|
||||
displayName: 'Checkpoint',
|
||||
singularName: 'checkpoint',
|
||||
defaultPageSize: 50,
|
||||
supportsLetterFilter: false,
|
||||
supportsBulkOperations: true,
|
||||
supportsMove: false,
|
||||
templateName: 'checkpoints.html'
|
||||
},
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
displayName: 'Embedding',
|
||||
singularName: 'embedding',
|
||||
defaultPageSize: 100,
|
||||
supportsLetterFilter: true,
|
||||
supportsBulkOperations: true,
|
||||
supportsMove: true,
|
||||
templateName: 'embeddings.html'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate API endpoints for a given model type
|
||||
* @param {string} modelType - The model type (e.g., 'loras', 'checkpoints')
|
||||
* @returns {Object} Object containing all API endpoints for the model type
|
||||
*/
|
||||
export function getApiEndpoints(modelType) {
|
||||
if (!Object.values(MODEL_TYPES).includes(modelType)) {
|
||||
throw new Error(`Invalid model type: ${modelType}`);
|
||||
}
|
||||
|
||||
return {
|
||||
// Base CRUD operations
|
||||
list: `/api/${modelType}`,
|
||||
delete: `/api/${modelType}/delete`,
|
||||
exclude: `/api/${modelType}/exclude`,
|
||||
rename: `/api/${modelType}/rename`,
|
||||
save: `/api/${modelType}/save-metadata`,
|
||||
|
||||
// Bulk operations
|
||||
bulkDelete: `/api/${modelType}/bulk-delete`,
|
||||
|
||||
// CivitAI integration
|
||||
fetchCivitai: `/api/${modelType}/fetch-civitai`,
|
||||
fetchAllCivitai: `/api/${modelType}/fetch-all-civitai`,
|
||||
relinkCivitai: `/api/${modelType}/relink-civitai`,
|
||||
civitaiVersions: `/api/${modelType}/civitai/versions`,
|
||||
|
||||
// Preview management
|
||||
replacePreview: `/api/${modelType}/replace-preview`,
|
||||
|
||||
// Query operations
|
||||
scan: `/api/${modelType}/scan`,
|
||||
topTags: `/api/${modelType}/top-tags`,
|
||||
baseModels: `/api/${modelType}/base-models`,
|
||||
roots: `/api/${modelType}/roots`,
|
||||
folders: `/api/${modelType}/folders`,
|
||||
duplicates: `/api/${modelType}/find-duplicates`,
|
||||
conflicts: `/api/${modelType}/find-filename-conflicts`,
|
||||
verify: `/api/${modelType}/verify-duplicates`,
|
||||
|
||||
// Model-specific endpoints (will be merged with specific configs)
|
||||
specific: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Model-specific endpoint configurations
|
||||
*/
|
||||
export const MODEL_SPECIFIC_ENDPOINTS = {
|
||||
[MODEL_TYPES.LORA]: {
|
||||
letterCounts: `/api/${MODEL_TYPES.LORA}/letter-counts`,
|
||||
notes: `/api/${MODEL_TYPES.LORA}/get-notes`,
|
||||
triggerWords: `/api/${MODEL_TYPES.LORA}/get-trigger-words`,
|
||||
previewUrl: `/api/${MODEL_TYPES.LORA}/preview-url`,
|
||||
civitaiUrl: `/api/${MODEL_TYPES.LORA}/civitai-url`,
|
||||
modelDescription: `/api/${MODEL_TYPES.LORA}/model-description`,
|
||||
moveModel: `/api/${MODEL_TYPES.LORA}/move_model`,
|
||||
moveBulk: `/api/${MODEL_TYPES.LORA}/move_models_bulk`,
|
||||
getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`,
|
||||
civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`,
|
||||
civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`
|
||||
},
|
||||
[MODEL_TYPES.CHECKPOINT]: {
|
||||
info: `/api/${MODEL_TYPES.CHECKPOINT}/info`
|
||||
},
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
// Future embedding-specific endpoints
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get complete API configuration for a model type
|
||||
* @param {string} modelType - The model type
|
||||
* @returns {Object} Complete API configuration
|
||||
*/
|
||||
export function getCompleteApiConfig(modelType) {
|
||||
const baseEndpoints = getApiEndpoints(modelType);
|
||||
const specificEndpoints = MODEL_SPECIFIC_ENDPOINTS[modelType] || {};
|
||||
const config = MODEL_CONFIG[modelType];
|
||||
|
||||
return {
|
||||
modelType,
|
||||
config,
|
||||
endpoints: {
|
||||
...baseEndpoints,
|
||||
specific: specificEndpoints
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a model type is supported
|
||||
* @param {string} modelType - The model type to validate
|
||||
* @returns {boolean} True if valid, false otherwise
|
||||
*/
|
||||
export function isValidModelType(modelType) {
|
||||
return Object.values(MODEL_TYPES).includes(modelType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model type from current page or explicit parameter
|
||||
* @param {string} [explicitType] - Explicitly provided model type
|
||||
* @returns {string} The model type
|
||||
*/
|
||||
export function getCurrentModelType(explicitType = null) {
|
||||
if (explicitType && isValidModelType(explicitType)) {
|
||||
return explicitType;
|
||||
}
|
||||
|
||||
return state.currentPageType || MODEL_TYPES.LORA;
|
||||
}
|
||||
|
||||
// Download API endpoints (shared across all model types)
|
||||
export const DOWNLOAD_ENDPOINTS = {
|
||||
download: '/api/download-model',
|
||||
downloadGet: '/api/download-model-get',
|
||||
cancelGet: '/api/cancel-download-get',
|
||||
progress: '/api/download-progress'
|
||||
};
|
||||
|
||||
// WebSocket endpoints
|
||||
export const WS_ENDPOINTS = {
|
||||
fetchProgress: '/ws/fetch-progress'
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +1,43 @@
|
||||
import {
|
||||
fetchModelsPage,
|
||||
resetAndReloadWithVirtualScroll,
|
||||
loadMoreWithVirtualScroll,
|
||||
refreshModels as baseRefreshModels,
|
||||
deleteModel as baseDeleteModel,
|
||||
replaceModelPreview,
|
||||
fetchCivitaiMetadata,
|
||||
refreshSingleModelMetadata,
|
||||
excludeModel as baseExcludeModel
|
||||
} from './baseModelApi.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { createModelApiClient } from './baseModelApi.js';
|
||||
import { MODEL_TYPES } from './apiConfig.js';
|
||||
|
||||
/**
|
||||
* Fetch checkpoints with pagination for virtual scrolling
|
||||
* @param {number} page - Page number to fetch
|
||||
* @param {number} pageSize - Number of items per page
|
||||
* @returns {Promise<Object>} Object containing items, total count, and pagination info
|
||||
*/
|
||||
export async function fetchCheckpointsPage(page = 1, pageSize = 100) {
|
||||
return fetchModelsPage({
|
||||
modelType: 'checkpoint',
|
||||
page,
|
||||
pageSize,
|
||||
endpoint: '/api/checkpoints'
|
||||
});
|
||||
}
|
||||
// Create Checkpoint-specific API client
|
||||
const checkpointApiClient = createModelApiClient(MODEL_TYPES.CHECKPOINT);
|
||||
|
||||
/**
|
||||
* Load more checkpoints with pagination - updated to work with VirtualScroller
|
||||
* @param {boolean} resetPage - Whether to reset to the first page
|
||||
* @param {boolean} updateFolders - Whether to update folder tags
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
// Export all common operations using the unified client
|
||||
export const deleteModel = (filePath) => checkpointApiClient.deleteModel(filePath);
|
||||
export const excludeCheckpoint = (filePath) => checkpointApiClient.excludeModel(filePath);
|
||||
export const renameCheckpointFile = (filePath, newFileName) => checkpointApiClient.renameModelFile(filePath, newFileName);
|
||||
export const replacePreview = (filePath) => checkpointApiClient.replaceModelPreview(filePath);
|
||||
export const saveModelMetadata = (filePath, data) => checkpointApiClient.saveModelMetadata(filePath, data);
|
||||
export const refreshCheckpoints = (fullRebuild = false) => checkpointApiClient.refreshModels(fullRebuild);
|
||||
export const refreshSingleCheckpointMetadata = (filePath) => checkpointApiClient.refreshSingleModelMetadata(filePath);
|
||||
export const fetchCivitai = (resetAndReloadFunction) => checkpointApiClient.fetchCivitaiMetadata(resetAndReloadFunction);
|
||||
|
||||
// Pagination functions
|
||||
export const fetchCheckpointsPage = (page = 1, pageSize = 50) => checkpointApiClient.fetchModelsPage(page, pageSize);
|
||||
|
||||
// Virtual scrolling operations
|
||||
export async function loadMoreCheckpoints(resetPage = false, updateFolders = false) {
|
||||
return loadMoreWithVirtualScroll({
|
||||
modelType: 'checkpoint',
|
||||
resetPage,
|
||||
updateFolders,
|
||||
fetchPageFunction: fetchCheckpointsPage
|
||||
});
|
||||
return checkpointApiClient.loadMoreWithVirtualScroll(resetPage, updateFolders);
|
||||
}
|
||||
|
||||
// Reset and reload checkpoints
|
||||
export async function resetAndReload(updateFolders = false) {
|
||||
return resetAndReloadWithVirtualScroll({
|
||||
modelType: 'checkpoint',
|
||||
updateFolders,
|
||||
fetchPageFunction: fetchCheckpointsPage
|
||||
});
|
||||
return checkpointApiClient.resetAndReloadWithVirtualScroll(updateFolders);
|
||||
}
|
||||
|
||||
// Refresh checkpoints
|
||||
export async function refreshCheckpoints(fullRebuild = false) {
|
||||
return baseRefreshModels({
|
||||
modelType: 'checkpoint',
|
||||
scanEndpoint: '/api/checkpoints/scan',
|
||||
resetAndReloadFunction: resetAndReload,
|
||||
fullRebuild: fullRebuild
|
||||
});
|
||||
}
|
||||
|
||||
// Delete a checkpoint
|
||||
export function deleteCheckpoint(filePath) {
|
||||
return baseDeleteModel(filePath, 'checkpoint');
|
||||
}
|
||||
|
||||
// Replace checkpoint preview
|
||||
export function replaceCheckpointPreview(filePath) {
|
||||
return replaceModelPreview(filePath, 'checkpoint');
|
||||
}
|
||||
|
||||
// Fetch metadata from Civitai for checkpoints
|
||||
export async function fetchCivitai() {
|
||||
return fetchCivitaiMetadata({
|
||||
modelType: 'checkpoint',
|
||||
fetchEndpoint: '/api/checkpoints/fetch-all-civitai',
|
||||
resetAndReloadFunction: resetAndReload
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh single checkpoint metadata
|
||||
export async function refreshSingleCheckpointMetadata(filePath) {
|
||||
await refreshSingleModelMetadata(filePath, 'checkpoint');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save model metadata to the server
|
||||
* @param {string} filePath - Path to the model file
|
||||
* @param {Object} data - Metadata to save
|
||||
* @returns {Promise} - Promise that resolves with the server response
|
||||
*/
|
||||
export async function saveModelMetadata(filePath, data) {
|
||||
// Checkpoint-specific functions
|
||||
export async function getCheckpointInfo(name) {
|
||||
try {
|
||||
// Show loading indicator
|
||||
state.loadingManager.showSimpleLoading('Saving metadata...');
|
||||
|
||||
const response = await fetch('/api/checkpoints/save-metadata', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
...data
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save metadata');
|
||||
}
|
||||
|
||||
// Update the virtual scroller with the new metadata
|
||||
state.virtualScroller.updateSingleItem(filePath, data);
|
||||
|
||||
return response.json();
|
||||
} finally {
|
||||
// Always hide the loading indicator when done
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a checkpoint file
|
||||
* @param {string} filePath - Current file path
|
||||
* @param {string} newFileName - New file name (without path)
|
||||
* @returns {Promise<Object>} - Promise that resolves with the server response
|
||||
*/
|
||||
export async function renameCheckpointFile(filePath, newFileName) {
|
||||
try {
|
||||
// Show loading indicator
|
||||
state.loadingManager.showSimpleLoading('Renaming checkpoint file...');
|
||||
|
||||
const response = await fetch('/api/checkpoints/rename', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
new_file_name: newFileName
|
||||
})
|
||||
});
|
||||
const response = await fetch(`${checkpointApiClient.apiConfig.endpoints.specific.info}/${encodeURIComponent(name)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
|
||||
throw new Error(`Failed to fetch checkpoint info: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error renaming checkpoint file:', error);
|
||||
console.error('Error fetching checkpoint info:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
@@ -1,131 +1,35 @@
|
||||
import {
|
||||
fetchModelsPage,
|
||||
resetAndReloadWithVirtualScroll,
|
||||
loadMoreWithVirtualScroll,
|
||||
refreshModels as baseRefreshModels,
|
||||
deleteModel as baseDeleteModel,
|
||||
replaceModelPreview,
|
||||
fetchCivitaiMetadata,
|
||||
refreshSingleModelMetadata,
|
||||
excludeModel as baseExcludeModel
|
||||
} from './baseModelApi.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { createModelApiClient } from './baseModelApi.js';
|
||||
import { MODEL_TYPES } from './apiConfig.js';
|
||||
|
||||
/**
|
||||
* Save model metadata to the server
|
||||
* @param {string} filePath - File path
|
||||
* @param {Object} data - Data to save
|
||||
* @returns {Promise} Promise of the save operation
|
||||
*/
|
||||
export async function saveModelMetadata(filePath, data) {
|
||||
try {
|
||||
// Show loading indicator
|
||||
state.loadingManager.showSimpleLoading('Saving metadata...');
|
||||
|
||||
const response = await fetch('/api/loras/save-metadata', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
...data
|
||||
})
|
||||
});
|
||||
// Create LoRA-specific API client
|
||||
const loraApiClient = createModelApiClient(MODEL_TYPES.LORA);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save metadata');
|
||||
}
|
||||
// Export all common operations using the unified client
|
||||
export const deleteModel = (filePath) => loraApiClient.deleteModel(filePath);
|
||||
export const excludeLora = (filePath) => loraApiClient.excludeModel(filePath);
|
||||
export const renameLoraFile = (filePath, newFileName) => loraApiClient.renameModelFile(filePath, newFileName);
|
||||
export const replacePreview = (filePath) => loraApiClient.replaceModelPreview(filePath);
|
||||
export const saveModelMetadata = (filePath, data) => loraApiClient.saveModelMetadata(filePath, data);
|
||||
export const refreshLoras = (fullRebuild = false) => loraApiClient.refreshModels(fullRebuild);
|
||||
export const refreshSingleLoraMetadata = (filePath) => loraApiClient.refreshSingleModelMetadata(filePath);
|
||||
export const fetchCivitai = (resetAndReloadFunction) => loraApiClient.fetchCivitaiMetadata(resetAndReloadFunction);
|
||||
|
||||
// Update the virtual scroller with the new data
|
||||
state.virtualScroller.updateSingleItem(filePath, data);
|
||||
|
||||
return response.json();
|
||||
} finally {
|
||||
// Always hide the loading indicator when done
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
// Pagination functions
|
||||
export const fetchLorasPage = (page = 1, pageSize = 100) => loraApiClient.fetchModelsPage(page, pageSize);
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more loras with pagination - updated to work with VirtualScroller
|
||||
* @param {boolean} resetPage - Whether to reset to the first page
|
||||
* @param {boolean} updateFolders - Whether to update folder tags
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
// Virtual scrolling operations
|
||||
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
||||
return loadMoreWithVirtualScroll({
|
||||
modelType: 'lora',
|
||||
resetPage,
|
||||
updateFolders,
|
||||
fetchPageFunction: fetchLorasPage
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch loras with pagination for virtual scrolling
|
||||
* @param {number} page - Page number to fetch
|
||||
* @param {number} pageSize - Number of items per page
|
||||
* @returns {Promise<Object>} Object containing items, total count, and pagination info
|
||||
*/
|
||||
export async function fetchLorasPage(page = 1, pageSize = 100) {
|
||||
return fetchModelsPage({
|
||||
modelType: 'lora',
|
||||
page,
|
||||
pageSize,
|
||||
endpoint: '/api/loras'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchCivitai() {
|
||||
return fetchCivitaiMetadata({
|
||||
modelType: 'lora',
|
||||
fetchEndpoint: '/api/loras/fetch-all-civitai',
|
||||
resetAndReloadFunction: resetAndReload
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteModel(filePath) {
|
||||
return baseDeleteModel(filePath, 'lora');
|
||||
}
|
||||
|
||||
export async function replacePreview(filePath) {
|
||||
return replaceModelPreview(filePath, 'lora');
|
||||
return loraApiClient.loadMoreWithVirtualScroll(resetPage, updateFolders);
|
||||
}
|
||||
|
||||
export async function resetAndReload(updateFolders = false) {
|
||||
return resetAndReloadWithVirtualScroll({
|
||||
modelType: 'lora',
|
||||
updateFolders,
|
||||
fetchPageFunction: fetchLorasPage
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshLoras(fullRebuild = false) {
|
||||
return baseRefreshModels({
|
||||
modelType: 'lora',
|
||||
scanEndpoint: '/api/loras/scan',
|
||||
resetAndReloadFunction: resetAndReload,
|
||||
fullRebuild: fullRebuild
|
||||
});
|
||||
}
|
||||
|
||||
export async function refreshSingleLoraMetadata(filePath) {
|
||||
await refreshSingleModelMetadata(filePath, 'lora');
|
||||
return loraApiClient.resetAndReloadWithVirtualScroll(updateFolders);
|
||||
}
|
||||
|
||||
// LoRA-specific functions that don't have common equivalents
|
||||
export async function fetchModelDescription(modelId, filePath) {
|
||||
try {
|
||||
const response = await fetch(`/api/loras/model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`);
|
||||
const response = await fetch(`${loraApiClient.apiConfig.endpoints.specific.modelDescription}?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch model description: ${response.statusText}`);
|
||||
@@ -138,38 +42,47 @@ export async function fetchModelDescription(modelId, filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a LoRA file
|
||||
* @param {string} filePath - Current file path
|
||||
* @param {string} newFileName - New file name (without path)
|
||||
* @returns {Promise<Object>} - Promise that resolves with the server response
|
||||
*/
|
||||
export async function renameLoraFile(filePath, newFileName) {
|
||||
// Move operations (LoRA-specific)
|
||||
export async function moveModel(filePath, targetPath) {
|
||||
try {
|
||||
// Show loading indicator
|
||||
state.loadingManager.showSimpleLoading('Renaming LoRA file...');
|
||||
|
||||
const response = await fetch('/api/loras/rename', {
|
||||
const response = await fetch(loraApiClient.apiConfig.endpoints.specific.moveModel, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
file_path: filePath,
|
||||
new_file_name: newFileName
|
||||
target_path: targetPath
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
|
||||
throw new Error('Failed to move model');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error renaming LoRA file:', error);
|
||||
console.error('Error moving model:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveModelsBulk(filePaths, targetPath) {
|
||||
try {
|
||||
const response = await fetch(loraApiClient.apiConfig.endpoints.specific.moveBulk, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
file_paths: filePaths,
|
||||
target_path: targetPath
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to move models');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error moving models in bulk:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Hide loading indicator
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import { RecipeCard } from '../components/RecipeCard.js';
|
||||
import {
|
||||
resetAndReloadWithVirtualScroll,
|
||||
loadMoreWithVirtualScroll
|
||||
} from './baseModelApi.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
|
||||
@@ -98,6 +94,98 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and reload models using virtual scrolling
|
||||
* @param {Object} options - Operation options
|
||||
* @returns {Promise<Object>} The fetch result
|
||||
*/
|
||||
export async function resetAndReloadWithVirtualScroll(options = {}) {
|
||||
const {
|
||||
modelType = 'lora',
|
||||
updateFolders = false,
|
||||
fetchPageFunction
|
||||
} = options;
|
||||
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
try {
|
||||
pageState.isLoading = true;
|
||||
|
||||
// Reset page counter
|
||||
pageState.currentPage = 1;
|
||||
|
||||
// Fetch the first page
|
||||
const result = await fetchPageFunction(1, pageState.pageSize || 50);
|
||||
|
||||
// Update the virtual scroller
|
||||
state.virtualScroller.refreshWithData(
|
||||
result.items,
|
||||
result.totalItems,
|
||||
result.hasMore
|
||||
);
|
||||
|
||||
// Update state
|
||||
pageState.hasMore = result.hasMore;
|
||||
pageState.currentPage = 2; // Next page will be 2
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error reloading ${modelType}s:`, error);
|
||||
showToast(`Failed to reload ${modelType}s: ${error.message}`, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
pageState.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more models using virtual scrolling
|
||||
* @param {Object} options - Operation options
|
||||
* @returns {Promise<Object>} The fetch result
|
||||
*/
|
||||
export async function loadMoreWithVirtualScroll(options = {}) {
|
||||
const {
|
||||
modelType = 'lora',
|
||||
resetPage = false,
|
||||
updateFolders = false,
|
||||
fetchPageFunction
|
||||
} = options;
|
||||
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
try {
|
||||
// Start loading state
|
||||
pageState.isLoading = true;
|
||||
|
||||
// Reset to first page if requested
|
||||
if (resetPage) {
|
||||
pageState.currentPage = 1;
|
||||
}
|
||||
|
||||
// Fetch the first page of data
|
||||
const result = await fetchPageFunction(pageState.currentPage, pageState.pageSize || 50);
|
||||
|
||||
// Update virtual scroller with the new data
|
||||
state.virtualScroller.refreshWithData(
|
||||
result.items,
|
||||
result.totalItems,
|
||||
result.hasMore
|
||||
);
|
||||
|
||||
// Update state
|
||||
pageState.hasMore = result.hasMore;
|
||||
pageState.currentPage = 2; // Next page to load would be 2
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error loading ${modelType}s:`, error);
|
||||
showToast(`Failed to load ${modelType}s: ${error.message}`, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
pageState.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and reload recipes using virtual scrolling
|
||||
* @param {boolean} updateFolders - Whether to update folder tags
|
||||
|
||||
@@ -5,18 +5,19 @@ import { loadMoreCheckpoints } from './api/checkpointApi.js';
|
||||
import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js';
|
||||
import { CheckpointContextMenu } from './components/ContextMenu/index.js';
|
||||
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||
import { MODEL_TYPES } from './api/apiConfig.js';
|
||||
|
||||
// Initialize the Checkpoints page
|
||||
class CheckpointsPageManager {
|
||||
constructor() {
|
||||
// Initialize page controls
|
||||
this.pageControls = createPageControls('checkpoints');
|
||||
this.pageControls = createPageControls(MODEL_TYPES.CHECKPOINT);
|
||||
|
||||
// Initialize checkpoint download manager
|
||||
window.checkpointDownloadManager = new CheckpointDownloadManager();
|
||||
|
||||
// Initialize the ModelDuplicatesManager
|
||||
this.duplicatesManager = new ModelDuplicatesManager(this, 'checkpoints');
|
||||
this.duplicatesManager = new ModelDuplicatesManager(this, MODEL_TYPES.CHECKPOINT);
|
||||
|
||||
// Expose only necessary functions to global scope
|
||||
this._exposeRequiredGlobalFunctions();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||
import { refreshSingleCheckpointMetadata, saveModelMetadata, replaceCheckpointPreview, resetAndReload } from '../../api/checkpointApi.js';
|
||||
import { resetAndReload } from '../../api/checkpointApi.js';
|
||||
import { getModelApiClient } from '../../api/baseModelApi.js';
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { showExcludeModal } from '../../utils/modalUtils.js';
|
||||
|
||||
@@ -19,7 +20,7 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
||||
|
||||
// Implementation needed by the mixin
|
||||
async saveModelMetadata(filePath, data) {
|
||||
return saveModelMetadata(filePath, data);
|
||||
return getModelApiClient().saveModelMetadata(filePath, data);
|
||||
}
|
||||
|
||||
handleMenuAction(action) {
|
||||
@@ -28,6 +29,8 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiClient = getModelApiClient();
|
||||
|
||||
// Otherwise handle checkpoint-specific actions
|
||||
switch(action) {
|
||||
case 'details':
|
||||
@@ -36,7 +39,7 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
||||
break;
|
||||
case 'replace-preview':
|
||||
// Add new action for replacing preview images
|
||||
replaceCheckpointPreview(this.currentCard.dataset.filepath);
|
||||
apiClient.replaceModelPreview(this.currentCard.dataset.filepath);
|
||||
break;
|
||||
case 'delete':
|
||||
// Delete checkpoint
|
||||
@@ -52,14 +55,14 @@ export class CheckpointContextMenu extends BaseContextMenu {
|
||||
break;
|
||||
case 'refresh-metadata':
|
||||
// Refresh metadata from CivitAI
|
||||
refreshSingleCheckpointMetadata(this.currentCard.dataset.filepath);
|
||||
apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath);
|
||||
break;
|
||||
case 'move':
|
||||
// Move to folder (placeholder)
|
||||
showToast('Move to folder feature coming soon', 'info');
|
||||
break;
|
||||
case 'exclude':
|
||||
showExcludeModal(this.currentCard.dataset.filepath, 'checkpoint');
|
||||
showExcludeModal(this.currentCard.dataset.filepath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { showModelModal } from './ModelModal.js';
|
||||
import { bulkManager } from '../../managers/BulkManager.js';
|
||||
import { modalManager } from '../../managers/ModalManager.js';
|
||||
import { NSFW_LEVELS } from '../../utils/constants.js';
|
||||
import { replacePreview, saveModelMetadata as saveLoraMetadata } from '../../api/loraApi.js';
|
||||
import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata as saveCheckpointMetadata } from '../../api/checkpointApi.js';
|
||||
import { getModelApiClient } from '../../api/baseModelApi.js';
|
||||
import { showDeleteModal } from '../../utils/modalUtils.js';
|
||||
|
||||
// Add global event delegation handlers
|
||||
@@ -31,6 +30,8 @@ function handleModelCardEvent_internal(event, modelType) {
|
||||
// Find the closest card element
|
||||
const card = event.target.closest('.lora-card');
|
||||
if (!card) return;
|
||||
|
||||
const apiClient = getModelApiClient();
|
||||
|
||||
// Handle specific elements within the card
|
||||
if (event.target.closest('.toggle-blur-btn')) {
|
||||
@@ -73,13 +74,13 @@ function handleModelCardEvent_internal(event, modelType) {
|
||||
|
||||
if (event.target.closest('.fa-trash')) {
|
||||
event.stopPropagation();
|
||||
showDeleteModal(card.dataset.filepath, modelType);
|
||||
showDeleteModal(card.dataset.filepath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest('.fa-image')) {
|
||||
event.stopPropagation();
|
||||
handleReplacePreview(card.dataset.filepath, modelType);
|
||||
apiClient.replaceModelPreview(card.dataset.filepath);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,9 +137,7 @@ async function toggleFavorite(card, modelType) {
|
||||
const newFavoriteState = !isFavorite;
|
||||
|
||||
try {
|
||||
// Use the appropriate save function based on model type
|
||||
const saveFunction = modelType === 'lora' ? saveLoraMetadata : saveCheckpointMetadata;
|
||||
await saveFunction(card.dataset.filepath, {
|
||||
await apiClient.saveModelMetadata(card.dataset.filepath, {
|
||||
favorite: newFavoriteState
|
||||
});
|
||||
|
||||
@@ -179,15 +178,7 @@ function handleCopyAction(card, modelType) {
|
||||
}
|
||||
|
||||
function handleReplacePreview(filePath, modelType) {
|
||||
if (modelType === 'lora') {
|
||||
replacePreview(filePath);
|
||||
} else {
|
||||
if (window.replaceCheckpointPreview) {
|
||||
window.replaceCheckpointPreview(filePath);
|
||||
} else {
|
||||
apiReplaceCheckpointPreview(filePath);
|
||||
}
|
||||
}
|
||||
apiClient.replaceModelPreview(filePath);
|
||||
}
|
||||
|
||||
async function handleExampleImagesAccess(card, modelType) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BASE_MODELS } from '../../utils/constants.js';
|
||||
import { state } from '../../state/index.js';
|
||||
import { saveModelMetadata as saveLoraMetadata, renameLoraFile } from '../../api/loraApi.js';
|
||||
import { saveModelMetadata as saveCheckpointMetadata, renameCheckpointFile } from '../../api/checkpointApi.js';
|
||||
import { getModelApiClient } from '../../api/baseModelApi.js';
|
||||
|
||||
/**
|
||||
* Set up model name editing functionality
|
||||
@@ -114,9 +115,7 @@ export function setupModelNameEditing(filePath) {
|
||||
// Get the file path from the dataset
|
||||
const filePath = this.dataset.filePath;
|
||||
|
||||
const saveFunction = state.currentPageType === 'checkpoints' ? saveCheckpointMetadata : saveLoraMetadata;
|
||||
|
||||
await saveFunction(filePath, { model_name: newModelName });
|
||||
await getModelApiClient().saveModelMetadata(filePath, { model_name: newModelName });
|
||||
|
||||
showToast('Model name updated successfully', 'success');
|
||||
} catch (error) {
|
||||
@@ -295,9 +294,7 @@ async function saveBaseModel(filePath, originalValue) {
|
||||
}
|
||||
|
||||
try {
|
||||
const saveFunction = state.currentPageType === 'checkpoints' ? saveCheckpointMetadata : saveLoraMetadata;
|
||||
|
||||
await saveFunction(filePath, { base_model: newBaseModel });
|
||||
await getModelApiClient().saveModelMetadata(filePath, { base_model: newBaseModel });
|
||||
|
||||
showToast('Base model updated successfully', 'success');
|
||||
} catch (error) {
|
||||
@@ -417,29 +414,7 @@ export function setupFileNameEditing(filePath) {
|
||||
// Get the file path from the dataset
|
||||
const filePath = this.dataset.filePath;
|
||||
|
||||
let result;
|
||||
|
||||
if (state.currentPageType === 'checkpoints') {
|
||||
result = await renameCheckpointFile(filePath, newFileName);
|
||||
} else {
|
||||
// Use LoRA rename function
|
||||
result = await renameLoraFile(filePath, newFileName);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
showToast('File name updated successfully', 'success');
|
||||
|
||||
// Update virtual scroller if available (mainly for LoRAs)
|
||||
if (state.virtualScroller && typeof state.virtualScroller.updateSingleItem === 'function') {
|
||||
const newFilePath = filePath.replace(originalValue, newFileName);
|
||||
state.virtualScroller.updateSingleItem(filePath, {
|
||||
file_name: newFileName,
|
||||
file_path: newFilePath
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error || 'Unknown error');
|
||||
}
|
||||
await getModelApiClient().renameModelFile(filePath, newFileName);
|
||||
} catch (error) {
|
||||
console.error('Error renaming file:', error);
|
||||
this.textContent = originalValue; // Restore original file name
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import { showToast, copyToClipboard } from '../../../utils/uiHelpers.js';
|
||||
import { state } from '../../../state/index.js';
|
||||
import { uploadPreview } from '../../../api/baseModelApi.js';
|
||||
import { getModelApiClient } from '../../../api/baseModelApi.js';
|
||||
|
||||
/**
|
||||
* Try to load local image first, fall back to remote if local fails
|
||||
@@ -515,6 +515,7 @@ function initSetPreviewHandlers(container) {
|
||||
|
||||
// Get local file path if available
|
||||
const useLocalFile = mediaElement.dataset.localSrc && !mediaElement.dataset.localSrc.includes('undefined');
|
||||
const apiClient = getModelApiClient();
|
||||
|
||||
if (useLocalFile) {
|
||||
// We have a local file, use it directly
|
||||
@@ -523,7 +524,7 @@ function initSetPreviewHandlers(container) {
|
||||
const file = new File([blob], 'preview.jpg', { type: blob.type });
|
||||
|
||||
// Use the existing baseModelApi uploadPreview method with nsfw level
|
||||
await uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
||||
await apiClient.uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
||||
} else {
|
||||
// We need to download the remote file first
|
||||
const response = await fetch(mediaElement.src);
|
||||
@@ -531,7 +532,7 @@ function initSetPreviewHandlers(container) {
|
||||
const file = new File([blob], 'preview.jpg', { type: blob.type });
|
||||
|
||||
// Use the existing baseModelApi uploadPreview method with nsfw level
|
||||
await uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
||||
await apiClient.uploadPreview(modelFilePath, file, modelType, nsfwLevel);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting preview:', error);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { showToast, updateFolderTags } from '../utils/uiHelpers.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getStorageItem } from '../utils/storageHelpers.js';
|
||||
import { updateFolderTags } from '../api/baseModelApi.js';
|
||||
|
||||
class MoveManager {
|
||||
constructor() {
|
||||
@@ -219,7 +218,7 @@ class MoveManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/loras/move_model', {
|
||||
const response = await fetch('/api/move_model', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -257,7 +256,7 @@ class MoveManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/loras/move_models_bulk', {
|
||||
const response = await fetch('/api/move_models_bulk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// Create the new hierarchical state structure
|
||||
import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
|
||||
// Load settings from localStorage or use defaults
|
||||
const savedSettings = getStorageItem('settings', {
|
||||
blurMatureContent: true,
|
||||
show_only_sfw: false,
|
||||
cardInfoDisplay: 'always' // Add default value for card info display
|
||||
cardInfoDisplay: 'always'
|
||||
});
|
||||
|
||||
// Load preview versions from localStorage
|
||||
// Load preview versions from localStorage for each model type
|
||||
const loraPreviewVersions = getMapFromStorage('lora_preview_versions');
|
||||
const checkpointPreviewVersions = getMapFromStorage('checkpoint_preview_versions');
|
||||
const embeddingPreviewVersions = getMapFromStorage('embedding_preview_versions');
|
||||
|
||||
export const state = {
|
||||
// Global state
|
||||
@@ -22,13 +24,13 @@ export const state = {
|
||||
|
||||
// Page-specific states
|
||||
pages: {
|
||||
loras: {
|
||||
[MODEL_TYPES.LORA]: {
|
||||
currentPage: 1,
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'name',
|
||||
activeFolder: null,
|
||||
activeLetterFilter: null, // New property for letter filtering
|
||||
activeLetterFilter: null,
|
||||
previewVersions: loraPreviewVersions,
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
@@ -67,10 +69,10 @@ export const state = {
|
||||
},
|
||||
pageSize: 20,
|
||||
showFavoritesOnly: false,
|
||||
duplicatesMode: false, // Add flag for duplicates mode
|
||||
duplicatesMode: false,
|
||||
},
|
||||
|
||||
checkpoints: {
|
||||
[MODEL_TYPES.CHECKPOINT]: {
|
||||
currentPage: 1,
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
@@ -89,11 +91,34 @@ export const state = {
|
||||
},
|
||||
showFavoritesOnly: false,
|
||||
duplicatesMode: false,
|
||||
},
|
||||
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
currentPage: 1,
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'name',
|
||||
activeFolder: null,
|
||||
activeLetterFilter: null,
|
||||
previewVersions: embeddingPreviewVersions,
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
tags: false,
|
||||
recursive: false
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
},
|
||||
showFavoritesOnly: false,
|
||||
duplicatesMode: false,
|
||||
}
|
||||
},
|
||||
|
||||
// Current active page
|
||||
currentPageType: 'loras',
|
||||
// Current active page - use MODEL_TYPES constants
|
||||
currentPageType: MODEL_TYPES.LORA,
|
||||
|
||||
// Backward compatibility - proxy properties
|
||||
get currentPage() { return this.pages[this.currentPageType].currentPage; },
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { modalManager } from '../managers/ModalManager.js';
|
||||
import { excludeLora, deleteModel as deleteLora } from '../api/loraApi.js';
|
||||
import { excludeCheckpoint, deleteCheckpoint } from '../api/checkpointApi.js';
|
||||
import { getModelApiClient } from '../api/baseModelApi.js';
|
||||
|
||||
const apiClient = getModelApiClient();
|
||||
|
||||
let pendingDeletePath = null;
|
||||
let pendingModelType = null;
|
||||
let pendingExcludePath = null;
|
||||
let pendingExcludeModelType = null;
|
||||
|
||||
export function showDeleteModal(filePath, modelType = 'lora') {
|
||||
export function showDeleteModal(filePath) {
|
||||
pendingDeletePath = filePath;
|
||||
pendingModelType = modelType;
|
||||
|
||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||
@@ -29,12 +27,7 @@ export async function confirmDelete() {
|
||||
if (!pendingDeletePath) return;
|
||||
|
||||
try {
|
||||
// Use appropriate delete function based on model type
|
||||
if (pendingModelType === 'checkpoint') {
|
||||
await deleteCheckpoint(pendingDeletePath);
|
||||
} else {
|
||||
await deleteLora(pendingDeletePath);
|
||||
}
|
||||
await apiClient.deleteModel(pendingDeletePath);
|
||||
|
||||
closeDeleteModal();
|
||||
|
||||
@@ -54,9 +47,8 @@ export function closeDeleteModal() {
|
||||
}
|
||||
|
||||
// Functions for the exclude modal
|
||||
export function showExcludeModal(filePath, modelType = 'lora') {
|
||||
export function showExcludeModal(filePath) {
|
||||
pendingExcludePath = filePath;
|
||||
pendingExcludeModelType = modelType;
|
||||
|
||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
const modelName = card ? card.dataset.name : filePath.split('/').pop();
|
||||
@@ -82,12 +74,7 @@ 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);
|
||||
}
|
||||
await apiClient.excludeModel(pendingExcludePath);
|
||||
|
||||
closeExcludeModal();
|
||||
|
||||
|
||||
@@ -615,4 +615,31 @@ export async function openExampleImagesFolder(modelHash) {
|
||||
showToast('Failed to open example images folder', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the folder tags display with new folder list
|
||||
* @param {Array} folders - List of folder names
|
||||
*/
|
||||
export function updateFolderTags(folders) {
|
||||
const folderTagsContainer = document.querySelector('.folder-tags');
|
||||
if (!folderTagsContainer) return;
|
||||
|
||||
// Keep track of currently selected folder
|
||||
const currentFolder = this.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;
|
||||
|
||||
// Scroll active folder into view (no need to reattach click handlers)
|
||||
const activeTag = folderTagsContainer.querySelector(`.tag[data-folder="${currentFolder}"]`);
|
||||
if (activeTag) {
|
||||
activeTag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user