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:
Will Miao
2025-07-25 10:04:18 +08:00
parent 692796db46
commit d83fad6abc
15 changed files with 927 additions and 928 deletions

View File

@@ -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
View 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

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);

View File

@@ -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',

View File

@@ -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; },

View File

@@ -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();

View File

@@ -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' });
}
}