mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
Merge branch 'sort-by-usage-count' into main
This commit is contained in:
@@ -14,11 +14,11 @@ import { eventManager } from '../../utils/EventManager.js';
|
||||
// Helper function to get display name based on settings
|
||||
function getDisplayName(model) {
|
||||
const displayNameSetting = state.global.settings.model_name_display || 'model_name';
|
||||
|
||||
|
||||
if (displayNameSetting === 'file_name') {
|
||||
return model.file_name || model.model_name || 'Unknown Model';
|
||||
}
|
||||
|
||||
|
||||
return model.model_name || model.file_name || 'Unknown Model';
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ function getDisplayName(model) {
|
||||
export function setupModelCardEventDelegation(modelType) {
|
||||
// Remove any existing handler first
|
||||
eventManager.removeHandler('click', 'modelCard-delegation');
|
||||
|
||||
|
||||
// Register model card event delegation with event manager
|
||||
eventManager.addHandler('click', 'modelCard-delegation', (event) => {
|
||||
return handleModelCardEvent_internal(event, modelType);
|
||||
@@ -42,26 +42,26 @@ function handleModelCardEvent_internal(event, modelType) {
|
||||
// Find the closest card element
|
||||
const card = event.target.closest('.model-card');
|
||||
if (!card) return false; // Continue with other handlers
|
||||
|
||||
|
||||
// Handle specific elements within the card
|
||||
if (event.target.closest('.toggle-blur-btn')) {
|
||||
event.stopPropagation();
|
||||
toggleBlurContent(card);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.show-content-btn')) {
|
||||
event.stopPropagation();
|
||||
showBlurredContent(card);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.fa-star')) {
|
||||
event.stopPropagation();
|
||||
toggleFavorite(card);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.fa-globe')) {
|
||||
event.stopPropagation();
|
||||
if (card.dataset.from_civitai === 'true') {
|
||||
@@ -69,37 +69,37 @@ function handleModelCardEvent_internal(event, modelType) {
|
||||
}
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.fa-paper-plane')) {
|
||||
event.stopPropagation();
|
||||
handleSendToWorkflow(card, event.shiftKey, modelType);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.fa-copy')) {
|
||||
event.stopPropagation();
|
||||
handleCopyAction(card, modelType);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.fa-trash')) {
|
||||
event.stopPropagation();
|
||||
showDeleteModal(card.dataset.filepath);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.fa-image')) {
|
||||
event.stopPropagation();
|
||||
getModelApiClient().replaceModelPreview(card.dataset.filepath);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
if (event.target.closest('.fa-folder-open')) {
|
||||
event.stopPropagation();
|
||||
handleExampleImagesAccess(card, modelType);
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
|
||||
// If no specific element was clicked, handle the card click (show modal or toggle selection)
|
||||
handleCardClick(card, modelType);
|
||||
return false; // Continue with other handlers (e.g., bulk selection)
|
||||
@@ -110,14 +110,14 @@ function toggleBlurContent(card) {
|
||||
const preview = card.querySelector('.card-preview');
|
||||
const isBlurred = preview.classList.toggle('blurred');
|
||||
const icon = card.querySelector('.toggle-blur-btn i');
|
||||
|
||||
|
||||
// Update the icon based on blur state
|
||||
if (isBlurred) {
|
||||
icon.className = 'fas fa-eye';
|
||||
} else {
|
||||
icon.className = 'fas fa-eye-slash';
|
||||
}
|
||||
|
||||
|
||||
// Toggle the overlay visibility
|
||||
const overlay = card.querySelector('.nsfw-overlay');
|
||||
if (overlay) {
|
||||
@@ -128,13 +128,13 @@ function toggleBlurContent(card) {
|
||||
function showBlurredContent(card) {
|
||||
const preview = card.querySelector('.card-preview');
|
||||
preview.classList.remove('blurred');
|
||||
|
||||
|
||||
// Update the toggle button icon
|
||||
const toggleBtn = card.querySelector('.toggle-blur-btn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.querySelector('i').className = 'fas fa-eye-slash';
|
||||
}
|
||||
|
||||
|
||||
// Hide the overlay
|
||||
const overlay = card.querySelector('.nsfw-overlay');
|
||||
if (overlay) {
|
||||
@@ -146,10 +146,10 @@ async function toggleFavorite(card) {
|
||||
const starIcon = card.querySelector('.fa-star');
|
||||
const isFavorite = starIcon.classList.contains('fas');
|
||||
const newFavoriteState = !isFavorite;
|
||||
|
||||
|
||||
try {
|
||||
await getModelApiClient().saveModelMetadata(card.dataset.filepath, {
|
||||
favorite: newFavoriteState
|
||||
await getModelApiClient().saveModelMetadata(card.dataset.filepath, {
|
||||
favorite: newFavoriteState
|
||||
});
|
||||
|
||||
if (newFavoriteState) {
|
||||
@@ -239,11 +239,11 @@ function handleReplacePreview(filePath, modelType) {
|
||||
|
||||
async function handleExampleImagesAccess(card, modelType) {
|
||||
const modelHash = card.dataset.sha256;
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/lm/has-example-images?model_hash=${modelHash}`);
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.has_images) {
|
||||
openExampleImagesFolder(modelHash);
|
||||
} else {
|
||||
@@ -257,7 +257,7 @@ async function handleExampleImagesAccess(card, modelType) {
|
||||
|
||||
function handleCardClick(card, modelType) {
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
|
||||
if (state.bulkMode) {
|
||||
// Toggle selection using the bulk manager
|
||||
bulkManager.toggleCardSelection(card);
|
||||
@@ -294,7 +294,7 @@ async function showModelModalFromCard(card, modelType) {
|
||||
usage_tips: card.dataset.usage_tips,
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
await showModelModal(modelMeta, modelType);
|
||||
}
|
||||
|
||||
@@ -310,9 +310,9 @@ function showExampleAccessModal(card, modelType) {
|
||||
try {
|
||||
const metaData = JSON.parse(card.dataset.meta || '{}');
|
||||
hasRemoteExamples = metaData.images &&
|
||||
Array.isArray(metaData.images) &&
|
||||
metaData.images.length > 0 &&
|
||||
metaData.images[0].url;
|
||||
Array.isArray(metaData.images) &&
|
||||
metaData.images.length > 0 &&
|
||||
metaData.images[0].url;
|
||||
} catch (e) {
|
||||
console.error('Error parsing meta data:', e);
|
||||
}
|
||||
@@ -329,10 +329,10 @@ function showExampleAccessModal(card, modelType) {
|
||||
showToast('modelCard.exampleImages.missingHash', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Close the modal
|
||||
modalManager.closeModal('exampleAccessModal');
|
||||
|
||||
|
||||
try {
|
||||
// Use the appropriate model API client to download examples
|
||||
const apiClient = getModelApiClient(modelType);
|
||||
@@ -462,7 +462,7 @@ export function createModelCard(model, modelType) {
|
||||
if (model.civitai) {
|
||||
card.dataset.meta = JSON.stringify(model.civitai || {});
|
||||
}
|
||||
|
||||
|
||||
// Store tags if available
|
||||
if (model.tags && Array.isArray(model.tags)) {
|
||||
card.dataset.tags = JSON.stringify(model.tags);
|
||||
@@ -475,7 +475,7 @@ export function createModelCard(model, modelType) {
|
||||
// Store NSFW level if available
|
||||
const nsfwLevel = model.preview_nsfw_level !== undefined ? model.preview_nsfw_level : 0;
|
||||
card.dataset.nsfwLevel = nsfwLevel;
|
||||
|
||||
|
||||
// Determine if the preview should be blurred based on NSFW level and user settings
|
||||
const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
if (shouldBlur) {
|
||||
@@ -506,7 +506,7 @@ export function createModelCard(model, modelType) {
|
||||
|
||||
// Check if autoplayOnHover is enabled for video previews
|
||||
const autoplayOnHover = state.global?.settings?.autoplay_on_hover || false;
|
||||
const isVideo = previewUrl.endsWith('.mp4');
|
||||
const isVideo = previewUrl.endsWith('.mp4') || previewUrl.endsWith('.webm');
|
||||
const videoAttrs = [
|
||||
'controls',
|
||||
'muted',
|
||||
@@ -527,10 +527,10 @@ export function createModelCard(model, modelType) {
|
||||
}
|
||||
|
||||
// Generate action icons based on model type with i18n support
|
||||
const favoriteTitle = isFavorite ?
|
||||
const favoriteTitle = isFavorite ?
|
||||
translate('modelCard.actions.removeFromFavorites', {}, 'Remove from favorites') :
|
||||
translate('modelCard.actions.addToFavorites', {}, 'Add to favorites');
|
||||
const globeTitle = model.from_civitai ?
|
||||
const globeTitle = model.from_civitai ?
|
||||
translate('modelCard.actions.viewOnCivitai', {}, 'View on Civitai') :
|
||||
translate('modelCard.actions.notAvailableFromCivitai', {}, 'Not available from Civitai');
|
||||
let sendTitle;
|
||||
@@ -582,13 +582,13 @@ export function createModelCard(model, modelType) {
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||
${isVideo ?
|
||||
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}">`
|
||||
}
|
||||
${isVideo ?
|
||||
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}">`
|
||||
}
|
||||
<div class="card-header">
|
||||
${shouldBlur ?
|
||||
`<button class="toggle-blur-btn" title="${toggleBlurTitle}">
|
||||
${shouldBlur ?
|
||||
`<button class="toggle-blur-btn" title="${toggleBlurTitle}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>` : ''}
|
||||
<div class="card-header-info">
|
||||
@@ -629,7 +629,7 @@ export function createModelCard(model, modelType) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
// Add video auto-play on hover functionality if needed
|
||||
const videoElement = card.querySelector('video');
|
||||
if (videoElement) {
|
||||
@@ -765,7 +765,7 @@ function cleanupHoverHandlers(videoElement) {
|
||||
function requestSafePlay(videoElement) {
|
||||
const playPromise = videoElement.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') {
|
||||
playPromise.catch(() => {});
|
||||
playPromise.catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,16 +887,16 @@ export function configureModelCardVideo(videoElement, autoplayOnHover) {
|
||||
export function updateCardsForBulkMode(isBulkMode) {
|
||||
// Update the state
|
||||
state.bulkMode = isBulkMode;
|
||||
|
||||
|
||||
document.body.classList.toggle('bulk-mode', isBulkMode);
|
||||
|
||||
|
||||
// Get all lora cards - this can now be from the DOM or through the virtual scroller
|
||||
const loraCards = document.querySelectorAll('.model-card');
|
||||
|
||||
|
||||
loraCards.forEach(card => {
|
||||
// Get all action containers for this card
|
||||
const actions = card.querySelectorAll('.card-actions');
|
||||
|
||||
|
||||
// Handle display property based on mode
|
||||
if (isBulkMode) {
|
||||
// Hide actions when entering bulk mode
|
||||
@@ -911,12 +911,12 @@ export function updateCardsForBulkMode(isBulkMode) {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// If using virtual scroller, we need to rerender after toggling bulk mode
|
||||
if (state.virtualScroller && typeof state.virtualScroller.scheduleRender === 'function') {
|
||||
state.virtualScroller.scheduleRender();
|
||||
}
|
||||
|
||||
|
||||
// Apply selection state to cards if entering bulk mode
|
||||
if (isBulkMode) {
|
||||
bulkManager.applySelectionState();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { showToast, openCivitai } from '../../utils/uiHelpers.js';
|
||||
import { modalManager } from '../../managers/ModalManager.js';
|
||||
import {
|
||||
import {
|
||||
toggleShowcase,
|
||||
setupShowcaseScroll,
|
||||
setupShowcaseScroll,
|
||||
scrollToTop,
|
||||
loadExampleImages
|
||||
} from './showcase/ShowcaseView.js';
|
||||
import { setupTabSwitching } from './ModelDescription.js';
|
||||
import {
|
||||
setupModelNameEditing,
|
||||
setupBaseModelEditing,
|
||||
import {
|
||||
setupModelNameEditing,
|
||||
setupBaseModelEditing,
|
||||
setupFileNameEditing
|
||||
} from './ModelMetadata.js';
|
||||
import { setupTagEditMode } from './ModelTags.js';
|
||||
@@ -242,7 +242,7 @@ export async function showModelModal(model, modelType) {
|
||||
const modalTitle = model.model_name;
|
||||
cleanupNavigationShortcuts();
|
||||
detachModalHandlers(modalId);
|
||||
|
||||
|
||||
// Fetch complete civitai metadata
|
||||
let completeCivitaiData = model.civitai || {};
|
||||
if (model.file_path) {
|
||||
@@ -254,7 +254,7 @@ export async function showModelModal(model, modelType) {
|
||||
// Continue with existing data if fetch fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update model with complete civitai data
|
||||
const modelWithFullData = {
|
||||
...model,
|
||||
@@ -269,14 +269,14 @@ export async function showModelModal(model, modelType) {
|
||||
</div>`.trim() : '';
|
||||
const creatorInfoAction = modelWithFullData.civitai?.creator ? `
|
||||
<div class="creator-info" data-username="${modelWithFullData.civitai.creator.username}" data-action="view-creator" title="${translate('modals.model.actions.viewCreatorProfile', {}, 'View Creator Profile')}">
|
||||
${modelWithFullData.civitai.creator.image ?
|
||||
`<div class="creator-avatar">
|
||||
${modelWithFullData.civitai.creator.image ?
|
||||
`<div class="creator-avatar">
|
||||
<img src="${modelWithFullData.civitai.creator.image}" alt="${modelWithFullData.civitai.creator.username}" onerror="this.onerror=null; this.src='/loras_static/icons/user-placeholder.png';">
|
||||
</div>` :
|
||||
`<div class="creator-avatar creator-placeholder">
|
||||
</div>` :
|
||||
`<div class="creator-avatar creator-placeholder">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
<span class="creator-username">${modelWithFullData.civitai.creator.username}</span>
|
||||
</div>`.trim() : '';
|
||||
const creatorActionItems = [];
|
||||
@@ -310,10 +310,10 @@ export async function showModelModal(model, modelType) {
|
||||
const hasUpdateAvailable = Boolean(modelWithFullData.update_available);
|
||||
const updateAvailabilityState = { hasUpdateAvailable };
|
||||
const updateBadgeTooltip = translate('modelCard.badges.updateAvailable', {}, 'Update available');
|
||||
|
||||
|
||||
// Prepare LoRA specific data with complete civitai data
|
||||
const escapedWords = (modelType === 'loras' || modelType === 'embeddings') && modelWithFullData.civitai?.trainedWords?.length ?
|
||||
modelWithFullData.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : [];
|
||||
const escapedWords = (modelType === 'loras' || modelType === 'embeddings') && modelWithFullData.civitai?.trainedWords?.length ?
|
||||
modelWithFullData.civitai.trainedWords : [];
|
||||
|
||||
// Generate model type specific content
|
||||
let typeSpecificContent;
|
||||
@@ -343,7 +343,7 @@ export async function showModelModal(model, modelType) {
|
||||
${versionsTabBadge}
|
||||
</button>`.trim();
|
||||
|
||||
const tabsContent = modelType === 'loras' ?
|
||||
const tabsContent = modelType === 'loras' ?
|
||||
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
||||
<button class="tab-btn" data-tab="description">${descriptionText}</button>
|
||||
${versionsTabButton}
|
||||
@@ -351,12 +351,12 @@ export async function showModelModal(model, modelType) {
|
||||
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
||||
<button class="tab-btn" data-tab="description">${descriptionText}</button>
|
||||
${versionsTabButton}`;
|
||||
|
||||
|
||||
const loadingExampleImagesText = translate('modals.model.loading.exampleImages', {}, 'Loading example images...');
|
||||
const loadingDescriptionText = translate('modals.model.loading.description', {}, 'Loading model description...');
|
||||
const loadingRecipesText = translate('modals.model.loading.recipes', {}, 'Loading recipes...');
|
||||
const loadingExamplesText = translate('modals.model.loading.examples', {}, 'Loading examples...');
|
||||
|
||||
|
||||
const loadingVersionsText = translate('modals.model.loading.versions', {}, 'Loading versions...');
|
||||
const civitaiModelId = modelWithFullData.civitai?.modelId || '';
|
||||
const civitaiVersionId = modelWithFullData.civitai?.id || '';
|
||||
@@ -373,7 +373,7 @@ export async function showModelModal(model, modelType) {
|
||||
</button>
|
||||
</div>`.trim();
|
||||
|
||||
const tabPanesContent = modelType === 'loras' ?
|
||||
const tabPanesContent = modelType === 'loras' ?
|
||||
`<div id="showcase-tab" class="tab-pane active">
|
||||
<div class="example-images-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> ${loadingExampleImagesText}
|
||||
@@ -518,7 +518,7 @@ export async function showModelModal(model, modelType) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
function updateVersionsTabBadge(hasUpdate) {
|
||||
const modalElement = document.getElementById(modalId);
|
||||
if (!modalElement) return;
|
||||
@@ -594,10 +594,10 @@ export async function showModelModal(model, modelType) {
|
||||
updateVersionsTabBadge(hasUpdate);
|
||||
updateCardUpdateAvailability(hasUpdate);
|
||||
}
|
||||
|
||||
|
||||
let showcaseCleanup;
|
||||
|
||||
const onCloseCallback = function() {
|
||||
const onCloseCallback = function () {
|
||||
// Clean up all handlers when modal closes for LoRA
|
||||
const modalElement = document.getElementById(modalId);
|
||||
if (modalElement && modalElement._clickHandler) {
|
||||
@@ -610,7 +610,7 @@ export async function showModelModal(model, modelType) {
|
||||
}
|
||||
cleanupNavigationShortcuts();
|
||||
};
|
||||
|
||||
|
||||
modalManager.showModal(modalId, content, null, onCloseCallback);
|
||||
const activeModalElement = document.getElementById(modalId);
|
||||
if (activeModalElement) {
|
||||
@@ -643,17 +643,17 @@ export async function showModelModal(model, modelType) {
|
||||
setupEventHandlers(modelWithFullData.file_path, modelType);
|
||||
setupNavigationShortcuts(modelType);
|
||||
updateNavigationControls();
|
||||
|
||||
|
||||
// LoRA specific setup
|
||||
if (modelType === 'loras' || modelType === 'embeddings') {
|
||||
setupTriggerWordsEditMode();
|
||||
|
||||
|
||||
if (modelType == 'loras') {
|
||||
// Load recipes for this LoRA
|
||||
loadRecipesForLora(modelWithFullData.model_name, modelWithFullData.sha256);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load example images asynchronously - merge regular and custom images
|
||||
const regularImages = modelWithFullData.civitai?.images || [];
|
||||
const customImages = modelWithFullData.civitai?.customImages || [];
|
||||
@@ -707,17 +707,17 @@ function detachModalHandlers(modalId) {
|
||||
*/
|
||||
function setupEventHandlers(filePath, modelType) {
|
||||
const modalElement = document.getElementById('modelModal');
|
||||
|
||||
|
||||
// Remove existing event listeners first
|
||||
modalElement.removeEventListener('click', handleModalClick);
|
||||
|
||||
|
||||
// Create and store the handler function
|
||||
function handleModalClick(event) {
|
||||
const target = event.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
|
||||
const action = target.dataset.action;
|
||||
|
||||
|
||||
switch (action) {
|
||||
case 'close-modal':
|
||||
modalManager.closeModal('modelModal');
|
||||
@@ -748,10 +748,10 @@ function setupEventHandlers(filePath, modelType) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add the event listener with the named function
|
||||
modalElement.addEventListener('click', handleModalClick);
|
||||
|
||||
|
||||
// Store reference to the handler on the element for potential cleanup
|
||||
modalElement._clickHandler = handleModalClick;
|
||||
}
|
||||
@@ -763,15 +763,15 @@ function setupEventHandlers(filePath, modelType) {
|
||||
*/
|
||||
function setupEditableFields(filePath, modelType) {
|
||||
const editableFields = document.querySelectorAll('.editable-field [contenteditable]');
|
||||
|
||||
|
||||
editableFields.forEach(field => {
|
||||
field.addEventListener('focus', function() {
|
||||
field.addEventListener('focus', function () {
|
||||
if (this.textContent === 'Add your notes here...') {
|
||||
this.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
field.addEventListener('blur', function() {
|
||||
field.addEventListener('blur', function () {
|
||||
if (this.textContent.trim() === '') {
|
||||
if (this.classList.contains('notes-content')) {
|
||||
this.textContent = 'Add your notes here...';
|
||||
@@ -783,7 +783,7 @@ function setupEditableFields(filePath, modelType) {
|
||||
// Add keydown event listeners for notes
|
||||
const notesContent = document.querySelector('.notes-content');
|
||||
if (notesContent) {
|
||||
notesContent.addEventListener('keydown', async function(e) {
|
||||
notesContent.addEventListener('keydown', async function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.shiftKey) {
|
||||
// Allow shift+enter for new line
|
||||
@@ -810,7 +810,7 @@ function setupLoraSpecificFields(filePath) {
|
||||
|
||||
if (!presetSelector || !presetValue || !addPresetBtn || !presetTags) return;
|
||||
|
||||
presetSelector.addEventListener('change', function() {
|
||||
presetSelector.addEventListener('change', function () {
|
||||
const selected = this.value;
|
||||
if (selected) {
|
||||
presetValue.style.display = 'inline-block';
|
||||
@@ -828,10 +828,10 @@ function setupLoraSpecificFields(filePath) {
|
||||
}
|
||||
});
|
||||
|
||||
addPresetBtn.addEventListener('click', async function() {
|
||||
addPresetBtn.addEventListener('click', async function () {
|
||||
const key = presetSelector.value;
|
||||
const value = presetValue.value;
|
||||
|
||||
|
||||
if (!key || !value) return;
|
||||
|
||||
const currentPath = resolveFilePath();
|
||||
@@ -839,21 +839,21 @@ function setupLoraSpecificFields(filePath) {
|
||||
const loraCard = document.querySelector(`.model-card[data-filepath="${currentPath}"]`) ||
|
||||
document.querySelector(`.model-card[data-filepath="${filePath}"]`);
|
||||
const currentPresets = parsePresets(loraCard?.dataset.usage_tips);
|
||||
|
||||
|
||||
currentPresets[key] = parseFloat(value);
|
||||
const newPresetsJson = JSON.stringify(currentPresets);
|
||||
|
||||
await getModelApiClient().saveModelMetadata(currentPath, { usage_tips: newPresetsJson });
|
||||
|
||||
presetTags.innerHTML = renderPresetTags(currentPresets);
|
||||
|
||||
|
||||
presetSelector.value = '';
|
||||
presetValue.value = '';
|
||||
presetValue.style.display = 'none';
|
||||
});
|
||||
|
||||
// Add keydown event for preset value
|
||||
presetValue.addEventListener('keydown', function(e) {
|
||||
presetValue.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addPresetBtn.click();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
|
||||
import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||
import { escapeAttribute } from './utils.js';
|
||||
import { escapeAttribute, escapeHtml } from './utils.js';
|
||||
|
||||
/**
|
||||
* Fetch trained words for a model
|
||||
@@ -17,7 +17,7 @@ async function fetchTrainedWords(filePath) {
|
||||
try {
|
||||
const response = await fetch(`/api/lm/trained-words?file_path=${encodeURIComponent(filePath)}`);
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
return {
|
||||
trainedWords: data.trained_words || [], // Returns array of [word, frequency] pairs
|
||||
@@ -43,11 +43,11 @@ async function fetchTrainedWords(filePath) {
|
||||
function createSuggestionDropdown(trainedWords, classTokens, existingWords = []) {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'metadata-suggestions-dropdown';
|
||||
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'metadata-suggestions-header';
|
||||
|
||||
|
||||
// No suggestions case
|
||||
if ((!trainedWords || trainedWords.length === 0) && !classTokens) {
|
||||
header.innerHTML = `<span>${translate('modals.model.triggerWords.suggestions.noSuggestions')}</span>`;
|
||||
@@ -55,12 +55,12 @@ function createSuggestionDropdown(trainedWords, classTokens, existingWords = [])
|
||||
dropdown.innerHTML += `<div class="no-suggestions">${translate('modals.model.triggerWords.suggestions.noTrainedWords')}</div>`;
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
|
||||
// Sort trained words by frequency (highest first) if available
|
||||
if (trainedWords && trainedWords.length > 0) {
|
||||
trainedWords.sort((a, b) => b[1] - a[1]);
|
||||
}
|
||||
|
||||
|
||||
// Add class tokens section if available
|
||||
if (classTokens) {
|
||||
// Add class tokens header
|
||||
@@ -71,45 +71,47 @@ function createSuggestionDropdown(trainedWords, classTokens, existingWords = [])
|
||||
<small>${translate('modals.model.triggerWords.suggestions.classTokenDescription')}</small>
|
||||
`;
|
||||
dropdown.appendChild(classTokensHeader);
|
||||
|
||||
|
||||
// Add class tokens container
|
||||
const classTokensContainer = document.createElement('div');
|
||||
classTokensContainer.className = 'class-tokens-container';
|
||||
|
||||
|
||||
// Create a special item for the class token
|
||||
const tokenItem = document.createElement('div');
|
||||
tokenItem.className = `metadata-suggestion-item class-token-item ${existingWords.includes(classTokens) ? 'already-added' : ''}`;
|
||||
tokenItem.title = `${translate('modals.model.triggerWords.suggestions.classToken')}: ${classTokens}`;
|
||||
|
||||
const escapedToken = escapeHtml(classTokens);
|
||||
tokenItem.innerHTML = `
|
||||
<span class="metadata-suggestion-text">${classTokens}</span>
|
||||
<span class="metadata-suggestion-text">${escapedToken}</span>
|
||||
<div class="metadata-suggestion-meta">
|
||||
<span class="token-badge">${translate('modals.model.triggerWords.suggestions.classToken')}</span>
|
||||
${existingWords.includes(classTokens) ?
|
||||
`<span class="added-indicator"><i class="fas fa-check"></i></span>` : ''}
|
||||
${existingWords.includes(classTokens) ?
|
||||
`<span class="added-indicator"><i class="fas fa-check"></i></span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
// Add click handler if not already added
|
||||
if (!existingWords.includes(classTokens)) {
|
||||
tokenItem.addEventListener('click', () => {
|
||||
// Automatically add this word
|
||||
addNewTriggerWord(classTokens);
|
||||
|
||||
|
||||
// Also populate the input field for potential editing
|
||||
const input = document.querySelector('.metadata-input');
|
||||
if (input) input.value = classTokens;
|
||||
|
||||
|
||||
// Focus on the input
|
||||
if (input) input.focus();
|
||||
|
||||
|
||||
// Update dropdown without removing it
|
||||
updateTrainedWordsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
classTokensContainer.appendChild(tokenItem);
|
||||
dropdown.appendChild(classTokensContainer);
|
||||
|
||||
|
||||
// Add separator if we also have trained words
|
||||
if (trainedWords && trainedWords.length > 0) {
|
||||
const separator = document.createElement('div');
|
||||
@@ -117,7 +119,7 @@ function createSuggestionDropdown(trainedWords, classTokens, existingWords = [])
|
||||
dropdown.appendChild(separator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add trained words header if we have any
|
||||
if (trainedWords && trainedWords.length > 0) {
|
||||
header.innerHTML = `
|
||||
@@ -125,52 +127,54 @@ function createSuggestionDropdown(trainedWords, classTokens, existingWords = [])
|
||||
<small>${translate('modals.model.triggerWords.suggestions.wordsFound', { count: trainedWords.length })}</small>
|
||||
`;
|
||||
dropdown.appendChild(header);
|
||||
|
||||
|
||||
// Create tag container for trained words
|
||||
const container = document.createElement('div');
|
||||
container.className = 'metadata-suggestions-container';
|
||||
|
||||
|
||||
// Add each trained word as a tag
|
||||
trainedWords.forEach(([word, frequency]) => {
|
||||
const isAdded = existingWords.includes(word);
|
||||
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = `metadata-suggestion-item ${isAdded ? 'already-added' : ''}`;
|
||||
item.title = word; // Show full word on hover if truncated
|
||||
|
||||
const escapedWord = escapeHtml(word);
|
||||
item.innerHTML = `
|
||||
<span class="metadata-suggestion-text">${word}</span>
|
||||
<span class="metadata-suggestion-text">${escapedWord}</span>
|
||||
<div class="metadata-suggestion-meta">
|
||||
<span class="trained-word-freq">${frequency}</span>
|
||||
${isAdded ? `<span class="added-indicator"><i class="fas fa-check"></i></span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
if (!isAdded) {
|
||||
item.addEventListener('click', () => {
|
||||
// Automatically add this word
|
||||
addNewTriggerWord(word);
|
||||
|
||||
|
||||
// Also populate the input field for potential editing
|
||||
const input = document.querySelector('.metadata-input');
|
||||
if (input) input.value = word;
|
||||
|
||||
|
||||
// Focus on the input
|
||||
if (input) input.focus();
|
||||
|
||||
|
||||
// Update dropdown without removing it
|
||||
updateTrainedWordsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
|
||||
dropdown.appendChild(container);
|
||||
} else if (!classTokens) {
|
||||
// If we have neither class tokens nor trained words
|
||||
dropdown.innerHTML += `<div class="no-suggestions">${translate('modals.model.triggerWords.suggestions.noTrainedWords')}</div>`;
|
||||
}
|
||||
|
||||
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
@@ -204,7 +208,7 @@ export function renderTriggerWords(words, filePath) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
return `
|
||||
<div class="info-item full-width trigger-words">
|
||||
<div class="trigger-words-header">
|
||||
@@ -215,9 +219,12 @@ export function renderTriggerWords(words, filePath) {
|
||||
</div>
|
||||
<div class="trigger-words-content">
|
||||
<div class="trigger-words-tags">
|
||||
${words.map(word => `
|
||||
<div class="trigger-word-tag" data-word="${word}" onclick="copyTriggerWord('${word}')" title="${translate('modals.model.triggerWords.copyWord')}">
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
${words.map(word => {
|
||||
const escapedWord = escapeHtml(word);
|
||||
const escapedAttr = escapeAttribute(word);
|
||||
return `
|
||||
<div class="trigger-word-tag" data-word="${escapedAttr}" onclick="copyTriggerWord(this.dataset.word)" title="${translate('modals.model.triggerWords.copyWord')}">
|
||||
<span class="trigger-word-content">${escapedWord}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
@@ -225,7 +232,7 @@ export function renderTriggerWords(words, filePath) {
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
`}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="metadata-edit-controls" style="display:none;">
|
||||
@@ -250,68 +257,68 @@ export function setupTriggerWordsEditMode() {
|
||||
let isTrainedWordsLoaded = false;
|
||||
// Store original trigger words for restoring on cancel
|
||||
let originalTriggerWords = [];
|
||||
|
||||
|
||||
const editBtn = document.querySelector('.edit-trigger-words-btn');
|
||||
if (!editBtn) return;
|
||||
|
||||
editBtn.addEventListener('click', async function() {
|
||||
|
||||
editBtn.addEventListener('click', async function () {
|
||||
const triggerWordsSection = this.closest('.trigger-words');
|
||||
const isEditMode = triggerWordsSection.classList.toggle('edit-mode');
|
||||
const filePath = this.dataset.filePath;
|
||||
|
||||
|
||||
// Toggle edit mode UI elements
|
||||
const triggerWordTags = triggerWordsSection.querySelectorAll('.trigger-word-tag');
|
||||
const editControls = triggerWordsSection.querySelector('.metadata-edit-controls');
|
||||
const addForm = triggerWordsSection.querySelector('.metadata-add-form');
|
||||
const noTriggerWords = triggerWordsSection.querySelector('.no-trigger-words');
|
||||
const tagsContainer = triggerWordsSection.querySelector('.trigger-words-tags');
|
||||
|
||||
|
||||
if (isEditMode) {
|
||||
this.innerHTML = '<i class="fas fa-times"></i>'; // Change to cancel icon
|
||||
this.title = translate('modals.model.triggerWords.cancel');
|
||||
|
||||
|
||||
// Store original trigger words for potential restoration
|
||||
originalTriggerWords = Array.from(triggerWordTags).map(tag => tag.dataset.word);
|
||||
|
||||
|
||||
// Show edit controls and input form
|
||||
editControls.style.display = 'flex';
|
||||
addForm.style.display = 'flex';
|
||||
|
||||
|
||||
// If we have no trigger words yet, hide the "No trigger word needed" text
|
||||
// and show the empty tags container
|
||||
if (noTriggerWords) {
|
||||
noTriggerWords.style.display = 'none';
|
||||
if (tagsContainer) tagsContainer.style.display = 'flex';
|
||||
}
|
||||
|
||||
|
||||
// Disable click-to-copy and show delete buttons
|
||||
triggerWordTags.forEach(tag => {
|
||||
tag.onclick = null;
|
||||
const copyIcon = tag.querySelector('.trigger-word-copy');
|
||||
const deleteBtn = tag.querySelector('.metadata-delete-btn');
|
||||
|
||||
|
||||
if (copyIcon) copyIcon.style.display = 'none';
|
||||
if (deleteBtn) {
|
||||
deleteBtn.style.display = 'block';
|
||||
|
||||
|
||||
// Re-attach event listener to ensure it works every time
|
||||
// First remove any existing listeners to avoid duplication
|
||||
deleteBtn.removeEventListener('click', deleteTriggerWord);
|
||||
deleteBtn.addEventListener('click', deleteTriggerWord);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Load trained words and display dropdown when entering edit mode
|
||||
// Add loading indicator
|
||||
const loadingIndicator = document.createElement('div');
|
||||
loadingIndicator.className = 'metadata-loading';
|
||||
loadingIndicator.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${translate('modals.model.triggerWords.suggestions.loading')}`;
|
||||
addForm.appendChild(loadingIndicator);
|
||||
|
||||
|
||||
// Get currently added trigger words
|
||||
const currentTags = triggerWordsSection.querySelectorAll('.trigger-word-tag');
|
||||
const existingWords = Array.from(currentTags).map(tag => tag.dataset.word);
|
||||
|
||||
|
||||
// Asynchronously load trained words if not already loaded
|
||||
if (!isTrainedWordsLoaded) {
|
||||
const result = await fetchTrainedWords(filePath);
|
||||
@@ -319,25 +326,25 @@ export function setupTriggerWordsEditMode() {
|
||||
classTokensValue = result.classTokens;
|
||||
isTrainedWordsLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
// Remove loading indicator
|
||||
loadingIndicator.remove();
|
||||
|
||||
|
||||
// Create and display suggestion dropdown
|
||||
const dropdown = createSuggestionDropdown(trainedWordsList, classTokensValue, existingWords);
|
||||
addForm.appendChild(dropdown);
|
||||
|
||||
|
||||
// Focus the input
|
||||
addForm.querySelector('input').focus();
|
||||
|
||||
|
||||
} else {
|
||||
this.innerHTML = '<i class="fas fa-pencil-alt"></i>'; // Change back to edit icon
|
||||
this.title = translate('modals.model.triggerWords.edit');
|
||||
|
||||
|
||||
// Hide edit controls and input form
|
||||
editControls.style.display = 'none';
|
||||
addForm.style.display = 'none';
|
||||
|
||||
|
||||
// Check if we're exiting edit mode due to "Save" or "Cancel"
|
||||
if (!this.dataset.skipRestore) {
|
||||
// If canceling, restore original trigger words
|
||||
@@ -348,7 +355,7 @@ export function setupTriggerWordsEditMode() {
|
||||
// Reset the skip restore flag
|
||||
delete this.dataset.skipRestore;
|
||||
}
|
||||
|
||||
|
||||
// If we have no trigger words, show the "No trigger word needed" text
|
||||
// and hide the empty tags container
|
||||
const currentTags = triggerWordsSection.querySelectorAll('.trigger-word-tag');
|
||||
@@ -356,19 +363,19 @@ export function setupTriggerWordsEditMode() {
|
||||
noTriggerWords.style.display = '';
|
||||
if (tagsContainer) tagsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
// Remove dropdown if present
|
||||
const dropdown = triggerWordsSection.querySelector('.metadata-suggestions-dropdown');
|
||||
if (dropdown) dropdown.remove();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Set up input for adding trigger words
|
||||
const triggerWordInput = document.querySelector('.metadata-input');
|
||||
|
||||
|
||||
if (triggerWordInput) {
|
||||
// Add keydown event to input
|
||||
triggerWordInput.addEventListener('keydown', function(e) {
|
||||
triggerWordInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewTriggerWord(this.value);
|
||||
@@ -376,13 +383,13 @@ export function setupTriggerWordsEditMode() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Set up save button
|
||||
const saveBtn = document.querySelector('.metadata-save-btn');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', saveTriggerWords);
|
||||
}
|
||||
|
||||
|
||||
// Set up delete buttons
|
||||
document.querySelectorAll('.metadata-delete-btn').forEach(btn => {
|
||||
// Remove any existing listeners to avoid duplication
|
||||
@@ -399,7 +406,7 @@ function deleteTriggerWord(e) {
|
||||
e.stopPropagation();
|
||||
const tag = this.closest('.trigger-word-tag');
|
||||
tag.remove();
|
||||
|
||||
|
||||
// Update status of items in the trained words dropdown
|
||||
updateTrainedWordsDropdown();
|
||||
}
|
||||
@@ -410,15 +417,15 @@ function deleteTriggerWord(e) {
|
||||
*/
|
||||
function resetTriggerWordsUIState(section) {
|
||||
const triggerWordTags = section.querySelectorAll('.trigger-word-tag');
|
||||
|
||||
|
||||
triggerWordTags.forEach(tag => {
|
||||
const word = tag.dataset.word;
|
||||
const copyIcon = tag.querySelector('.trigger-word-copy');
|
||||
const deleteBtn = tag.querySelector('.metadata-delete-btn');
|
||||
|
||||
|
||||
// Restore click-to-copy functionality
|
||||
tag.onclick = () => copyTriggerWord(word);
|
||||
|
||||
tag.onclick = () => copyTriggerWord(tag.dataset.word);
|
||||
|
||||
// Show copy icon, hide delete button
|
||||
if (copyIcon) copyIcon.style.display = '';
|
||||
if (deleteBtn) deleteBtn.style.display = 'none';
|
||||
@@ -433,30 +440,32 @@ function resetTriggerWordsUIState(section) {
|
||||
function restoreOriginalTriggerWords(section, originalWords) {
|
||||
const tagsContainer = section.querySelector('.trigger-words-tags');
|
||||
const noTriggerWords = section.querySelector('.no-trigger-words');
|
||||
|
||||
|
||||
if (!tagsContainer) return;
|
||||
|
||||
|
||||
// Clear current tags
|
||||
tagsContainer.innerHTML = '';
|
||||
|
||||
|
||||
if (originalWords.length === 0) {
|
||||
if (noTriggerWords) noTriggerWords.style.display = '';
|
||||
tagsContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Hide "no trigger words" message
|
||||
if (noTriggerWords) noTriggerWords.style.display = 'none';
|
||||
tagsContainer.style.display = 'flex';
|
||||
|
||||
|
||||
// Recreate original tags
|
||||
originalWords.forEach(word => {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'trigger-word-tag';
|
||||
tag.dataset.word = word;
|
||||
tag.onclick = () => copyTriggerWord(word);
|
||||
tag.onclick = () => copyTriggerWord(tag.dataset.word);
|
||||
|
||||
const escapedWord = escapeHtml(word);
|
||||
tag.innerHTML = `
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
<span class="trigger-word-content">${escapedWord}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
@@ -475,10 +484,10 @@ function restoreOriginalTriggerWords(section, originalWords) {
|
||||
function addNewTriggerWord(word) {
|
||||
word = word.trim();
|
||||
if (!word) return;
|
||||
|
||||
|
||||
const triggerWordsSection = document.querySelector('.trigger-words');
|
||||
let tagsContainer = document.querySelector('.trigger-words-tags');
|
||||
|
||||
|
||||
// Ensure tags container exists and is visible
|
||||
if (tagsContainer) {
|
||||
tagsContainer.style.display = 'flex';
|
||||
@@ -491,41 +500,43 @@ function addNewTriggerWord(word) {
|
||||
contentDiv.appendChild(tagsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!tagsContainer) return;
|
||||
|
||||
|
||||
// Hide "no trigger words" message if it exists
|
||||
const noTriggerWordsMsg = triggerWordsSection.querySelector('.no-trigger-words');
|
||||
if (noTriggerWordsMsg) {
|
||||
noTriggerWordsMsg.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
// Validation: Check length
|
||||
if (word.split(/\s+/).length > 100) {
|
||||
showToast('toast.triggerWords.tooLong', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validation: Check total number
|
||||
const currentTags = tagsContainer.querySelectorAll('.trigger-word-tag');
|
||||
if (currentTags.length >= 30) {
|
||||
showToast('toast.triggerWords.tooMany', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validation: Check for duplicates
|
||||
const existingWords = Array.from(currentTags).map(tag => tag.dataset.word);
|
||||
if (existingWords.includes(word)) {
|
||||
showToast('toast.triggerWords.alreadyExists', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create new tag
|
||||
const newTag = document.createElement('div');
|
||||
newTag.className = 'trigger-word-tag';
|
||||
newTag.dataset.word = word;
|
||||
|
||||
const escapedWord = escapeHtml(word);
|
||||
newTag.innerHTML = `
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
<span class="trigger-word-content">${escapedWord}</span>
|
||||
<span class="trigger-word-copy" style="display:none;">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
@@ -533,13 +544,13 @@ function addNewTriggerWord(word) {
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
|
||||
// Add event listener to delete button
|
||||
const deleteBtn = newTag.querySelector('.metadata-delete-btn');
|
||||
deleteBtn.addEventListener('click', deleteTriggerWord);
|
||||
|
||||
|
||||
tagsContainer.appendChild(newTag);
|
||||
|
||||
|
||||
// Update status of items in the trained words dropdown
|
||||
updateTrainedWordsDropdown();
|
||||
}
|
||||
@@ -550,19 +561,19 @@ function addNewTriggerWord(word) {
|
||||
function updateTrainedWordsDropdown() {
|
||||
const dropdown = document.querySelector('.metadata-suggestions-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
|
||||
// Get all current trigger words
|
||||
const currentTags = document.querySelectorAll('.trigger-word-tag');
|
||||
const existingWords = Array.from(currentTags).map(tag => tag.dataset.word);
|
||||
|
||||
|
||||
// Update status of each item in dropdown
|
||||
dropdown.querySelectorAll('.metadata-suggestion-item').forEach(item => {
|
||||
const wordText = item.querySelector('.metadata-suggestion-text').textContent;
|
||||
const isAdded = existingWords.includes(wordText);
|
||||
|
||||
|
||||
if (isAdded) {
|
||||
item.classList.add('already-added');
|
||||
|
||||
|
||||
// Add indicator if it doesn't exist
|
||||
let indicator = item.querySelector('.added-indicator');
|
||||
if (!indicator) {
|
||||
@@ -572,27 +583,27 @@ function updateTrainedWordsDropdown() {
|
||||
indicator.innerHTML = '<i class="fas fa-check"></i>';
|
||||
meta.appendChild(indicator);
|
||||
}
|
||||
|
||||
|
||||
// Remove click event
|
||||
item.onclick = null;
|
||||
} else {
|
||||
// Re-enable items that are no longer in the list
|
||||
item.classList.remove('already-added');
|
||||
|
||||
|
||||
// Remove indicator if it exists
|
||||
const indicator = item.querySelector('.added-indicator');
|
||||
if (indicator) indicator.remove();
|
||||
|
||||
|
||||
// Restore click event if not already set
|
||||
if (!item.onclick) {
|
||||
item.onclick = () => {
|
||||
const word = item.querySelector('.metadata-suggestion-text').textContent;
|
||||
addNewTriggerWord(word);
|
||||
|
||||
|
||||
// Also populate the input field
|
||||
const input = document.querySelector('.metadata-input');
|
||||
if (input) input.value = word;
|
||||
|
||||
|
||||
// Focus the input
|
||||
if (input) input.focus();
|
||||
};
|
||||
@@ -610,19 +621,19 @@ async function saveTriggerWords() {
|
||||
const triggerWordsSection = editBtn.closest('.trigger-words');
|
||||
const triggerWordTags = triggerWordsSection.querySelectorAll('.trigger-word-tag');
|
||||
const words = Array.from(triggerWordTags).map(tag => tag.dataset.word);
|
||||
|
||||
|
||||
try {
|
||||
// Special format for updating nested civitai.trainedWords
|
||||
await getModelApiClient().saveModelMetadata(filePath, {
|
||||
civitai: { trainedWords: words }
|
||||
});
|
||||
|
||||
|
||||
// Set flag to skip restoring original words when exiting edit mode
|
||||
editBtn.dataset.skipRestore = "true";
|
||||
|
||||
|
||||
// Exit edit mode without restoring original trigger words
|
||||
editBtn.click();
|
||||
|
||||
|
||||
// If we saved an empty array and there's a no-trigger-words element, show it
|
||||
const noTriggerWords = triggerWordsSection.querySelector('.no-trigger-words');
|
||||
const tagsContainer = triggerWordsSection.querySelector('.trigger-words-tags');
|
||||
@@ -630,7 +641,7 @@ async function saveTriggerWords() {
|
||||
noTriggerWords.style.display = '';
|
||||
if (tagsContainer) tagsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
showToast('toast.triggerWords.updateSuccess', {}, 'success');
|
||||
} catch (error) {
|
||||
console.error('Error saving trigger words:', error);
|
||||
@@ -642,7 +653,7 @@ async function saveTriggerWords() {
|
||||
* Copy a trigger word to clipboard
|
||||
* @param {string} word - Word to copy
|
||||
*/
|
||||
window.copyTriggerWord = async function(word) {
|
||||
window.copyTriggerWord = async function (word) {
|
||||
try {
|
||||
await copyToClipboard(word, 'Trigger word copied');
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user