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

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