feat: implement embeddings functionality with context menus, controls, and page management

This commit is contained in:
Will Miao
2025-07-25 23:15:33 +08:00
parent 12d1857b13
commit e4ce384023
12 changed files with 263 additions and 13 deletions

View File

@@ -0,0 +1,68 @@
import { BaseContextMenu } from './BaseContextMenu.js';
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
import { getModelApiClient, resetAndReload } from '../../api/baseModelApi.js';
import { showToast } from '../../utils/uiHelpers.js';
import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js';
export class EmbeddingContextMenu extends BaseContextMenu {
constructor() {
super('embeddingContextMenu', '.embedding-card');
this.nsfwSelector = document.getElementById('nsfwLevelSelector');
this.modelType = 'embedding';
this.resetAndReload = resetAndReload;
// Initialize NSFW Level Selector events
if (this.nsfwSelector) {
this.initNSFWSelector();
}
}
// Implementation needed by the mixin
async saveModelMetadata(filePath, data) {
return getModelApiClient().saveModelMetadata(filePath, data);
}
handleMenuAction(action) {
// First try to handle with common actions
if (ModelContextMenuMixin.handleCommonMenuActions.call(this, action)) {
return;
}
const apiClient = getModelApiClient();
// Otherwise handle embedding-specific actions
switch(action) {
case 'details':
// Show embedding details
this.currentCard.click();
break;
case 'replace-preview':
// Add new action for replacing preview images
apiClient.replaceModelPreview(this.currentCard.dataset.filepath);
break;
case 'delete':
showDeleteModal(this.currentCard.dataset.filepath);
break;
case 'copyname':
// Copy embedding name
if (this.currentCard.querySelector('.fa-copy')) {
this.currentCard.querySelector('.fa-copy').click();
}
break;
case 'refresh-metadata':
// Refresh metadata from CivitAI
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);
break;
}
}
}
// Mix in shared methods
Object.assign(EmbeddingContextMenu.prototype, ModelContextMenuMixin);

View File

@@ -1,4 +1,5 @@
export { LoraContextMenu } from './LoraContextMenu.js';
export { RecipeContextMenu } from './RecipeContextMenu.js';
export { CheckpointContextMenu } from './CheckpointContextMenu.js';
export { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
export { ModelContextMenuMixin } from './ModelContextMenuMixin.js';

View File

@@ -26,6 +26,7 @@ export class HeaderManager {
const path = window.location.pathname;
if (path.includes('/loras/recipes')) return 'recipes';
if (path.includes('/checkpoints')) return 'checkpoints';
if (path.includes('/embeddings')) return 'embeddings';
if (path.includes('/statistics')) return 'statistics';
if (path.includes('/loras')) return 'loras';
return 'unknown';

View File

@@ -0,0 +1,57 @@
// EmbeddingsControls.js - Specific implementation for the Embeddings page
import { PageControls } from './PageControls.js';
import { getModelApiClient, resetAndReload } from '../../api/baseModelApi.js';
import { showToast } from '../../utils/uiHelpers.js';
import { downloadManager } from '../../managers/DownloadManager.js';
/**
* EmbeddingsControls class - Extends PageControls for Embedding-specific functionality
*/
export class EmbeddingsControls extends PageControls {
constructor() {
// Initialize with 'embeddings' page type
super('embeddings');
// Register API methods specific to the Embeddings page
this.registerEmbeddingsAPI();
}
/**
* Register Embedding-specific API methods
*/
registerEmbeddingsAPI() {
const embeddingsAPI = {
// Core API functions
loadMoreModels: async (resetPage = false, updateFolders = false) => {
return await getModelApiClient().loadMoreWithVirtualScroll(resetPage, updateFolders);
},
resetAndReload: async (updateFolders = false) => {
return await resetAndReload(updateFolders);
},
refreshModels: async (fullRebuild = false) => {
return await getModelApiClient().refreshModels(fullRebuild);
},
// Add fetch from Civitai functionality for embeddings
fetchFromCivitai: async () => {
return await getModelApiClient().fetchCivitaiMetadata();
},
// Add show download modal functionality
showDownloadModal: () => {
downloadManager.showDownloadModal();
},
// No clearCustomFilter implementation is needed for embeddings
// as custom filters are currently only used for LoRAs
clearCustomFilter: async () => {
showToast('No custom filter to clear', 'info');
}
};
// Register the API
this.registerAPI(embeddingsAPI);
}
}

View File

@@ -2,13 +2,14 @@
import { PageControls } from './PageControls.js';
import { LorasControls } from './LorasControls.js';
import { CheckpointsControls } from './CheckpointsControls.js';
import { EmbeddingsControls } from './EmbeddingsControls.js';
// Export the classes
export { PageControls, LorasControls, CheckpointsControls };
export { PageControls, LorasControls, CheckpointsControls, EmbeddingsControls };
/**
* Factory function to create the appropriate controls based on page type
* @param {string} pageType - The type of page ('loras' or 'checkpoints')
* @param {string} pageType - The type of page ('loras', 'checkpoints', or 'embeddings')
* @returns {PageControls} - The appropriate controls instance
*/
export function createPageControls(pageType) {
@@ -16,6 +17,8 @@ export function createPageControls(pageType) {
return new LorasControls();
} else if (pageType === 'checkpoints') {
return new CheckpointsControls();
} else if (pageType === 'embeddings') {
return new EmbeddingsControls();
} else {
console.error(`Unknown page type: ${pageType}`);
return null;

View File

@@ -33,7 +33,15 @@ export function showModelModal(model, modelType) {
model.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : [];
// Generate model type specific content
const typeSpecificContent = modelType === 'loras' ? renderLoraSpecificContent(model, escapedWords) : '';
// const typeSpecificContent = modelType === 'loras' ? renderLoraSpecificContent(model, escapedWords) : '';
let typeSpecificContent;
if (modelType === 'loras') {
typeSpecificContent = renderLoraSpecificContent(model, escapedWords);
} else if (modelType === 'embeddings') {
typeSpecificContent = renderEmbeddingSpecificContent(model, escapedWords);
} else {
typeSpecificContent = '';
}
// Generate tabs based on model type
const tabsContent = modelType === 'loras' ?
@@ -248,6 +256,10 @@ function renderLoraSpecificContent(lora, escapedWords) {
`;
}
function renderEmbeddingSpecificContent(embedding, escapedWords) {
return `${renderTriggerWords(escapedWords, embedding.file_path)}`;
}
/**
* Sets up event handlers using event delegation for LoRA modal
* @param {string} filePath - Path to the model file

55
static/js/embeddings.js Normal file
View File

@@ -0,0 +1,55 @@
import { appCore } from './core.js';
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
import { createPageControls } from './components/controls/index.js';
import { EmbeddingContextMenu } from './components/ContextMenu/index.js';
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
import { MODEL_TYPES } from './api/apiConfig.js';
// Initialize the Embeddings page
class EmbeddingsPageManager {
constructor() {
// Initialize page controls
this.pageControls = createPageControls(MODEL_TYPES.EMBEDDING);
// Initialize the ModelDuplicatesManager
this.duplicatesManager = new ModelDuplicatesManager(this, MODEL_TYPES.EMBEDDING);
// Expose only necessary functions to global scope
this._exposeRequiredGlobalFunctions();
}
_exposeRequiredGlobalFunctions() {
// Minimal set of functions that need to remain global
window.confirmDelete = confirmDelete;
window.closeDeleteModal = closeDeleteModal;
window.confirmExclude = confirmExclude;
window.closeExcludeModal = closeExcludeModal;
// Expose duplicates manager
window.modelDuplicatesManager = this.duplicatesManager;
}
async initialize() {
// Initialize page-specific components
this.pageControls.restoreFolderFilter();
this.pageControls.initFolderTagsVisibility();
// Initialize context menu
new EmbeddingContextMenu();
// Initialize common page features
appCore.initializePageFeatures();
console.log('Embeddings Manager initialized');
}
}
// Initialize everything when DOM is ready
document.addEventListener('DOMContentLoaded', async () => {
// Initialize core application
await appCore.initialize();
// Initialize embeddings page
const embeddingsPage = new EmbeddingsPageManager();
await embeddingsPage.initialize();
});

View File

@@ -72,6 +72,8 @@ export class FilterManager {
tagsEndpoint = '/api/recipes/top-tags?limit=20';
} else if (this.currentPage === 'checkpoints') {
tagsEndpoint = '/api/checkpoints/top-tags?limit=20';
} else if (this.currentPage === 'embeddings') {
tagsEndpoint = '/api/embeddings/top-tags?limit=20';
}
const response = await fetch(tagsEndpoint);
@@ -147,6 +149,8 @@ export class FilterManager {
apiEndpoint = '/api/recipes/base-models';
} else if (this.currentPage === 'checkpoints') {
apiEndpoint = '/api/checkpoints/base-models';
} else if (this.currentPage === 'embeddings') {
apiEndpoint = '/api/embeddings/base-models';
} else {
return;
}
@@ -160,11 +164,7 @@ export class FilterManager {
data.base_models.forEach(model => {
const tag = document.createElement('div');
// Add base model classes only for the loras page
const baseModelClass = (this.currentPage === 'loras' && BASE_MODEL_CLASSES[model.name])
? BASE_MODEL_CLASSES[model.name]
: '';
tag.className = `filter-tag base-model-tag ${baseModelClass}`;
tag.className = `filter-tag base-model-tag`;
tag.dataset.baseModel = model.name;
tag.innerHTML = `${model.name} <span class="tag-count">${model.count}</span>`;

View File

@@ -313,7 +313,7 @@ export class SearchManager {
loraName: options.loraName || false,
loraModel: options.loraModel || false
};
} else if (this.currentPage === 'loras') {
} else if (this.currentPage === 'loras' || this.currentPage === 'embeddings') {
pageState.searchOptions = {
filename: options.filename || false,
modelname: options.modelname || false,