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