mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Add tag editing functionality: implement UI for editing model tags, including save and delete options, and integrate with existing modal structure.
This commit is contained in:
@@ -1540,4 +1540,285 @@
|
||||
height: 1px;
|
||||
background: var(--lora-border);
|
||||
margin: 5px 10px;
|
||||
}
|
||||
|
||||
/* Model Tags Edit Mode */
|
||||
.model-tags-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.edit-tags-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
padding: 2px 5px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
transition: all 0.2s ease;
|
||||
margin-left: var(--space-1);
|
||||
}
|
||||
|
||||
.edit-tags-btn.visible,
|
||||
.model-tags-container:hover .edit-tags-btn {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.edit-tags-btn:hover {
|
||||
opacity: 0.8 !important;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .edit-tags-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Edit mode active state */
|
||||
.model-tags-container.edit-mode {
|
||||
width: 100%;
|
||||
display: block; /* Change to block display in edit mode */
|
||||
flex-basis: 100%; /* Take full width in flex contexts */
|
||||
grid-column: 1 / -1; /* Ensure it spans full width in grid layouts */
|
||||
}
|
||||
|
||||
/* Fix for tags edit container width and position */
|
||||
.tags-edit-container {
|
||||
padding: var(--space-2);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--border-radius-sm);
|
||||
margin-top: var(--space-2);
|
||||
width: 100%; /* Ensure it takes full width */
|
||||
min-width: 100%; /* Force minimum width */
|
||||
max-width: 100%; /* Limit maximum width */
|
||||
box-sizing: border-box; /* Include padding in width calculation */
|
||||
position: relative; /* For proper positioning of elements inside */
|
||||
display: block; /* Force block display */
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tags-edit-container {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--lora-border);
|
||||
}
|
||||
|
||||
.tags-edit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--lora-border);
|
||||
width: 100%; /* Ensure header takes full width */
|
||||
}
|
||||
|
||||
/* Style for the edit button when positioned in the header */
|
||||
.edit-header-btn {
|
||||
display: inline-flex !important; /* Always show */
|
||||
opacity: 0.8 !important;
|
||||
color: var(--lora-accent) !important;
|
||||
margin-left: auto; /* Push to right */
|
||||
}
|
||||
|
||||
.tags-edit-content {
|
||||
margin-bottom: var(--space-1);
|
||||
width: 100%; /* Ensure full width */
|
||||
display: block; /* Force block display */
|
||||
}
|
||||
|
||||
.tags-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-2);
|
||||
width: 100%; /* Ensure full width */
|
||||
}
|
||||
|
||||
.tag-edit-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 4px 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tag-edit-content {
|
||||
color: var(--lora-accent) !important;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Delete button for tag */
|
||||
.delete-tag-btn {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--lora-error);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.delete-tag-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Edit controls */
|
||||
.tags-edit-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.tags-edit-controls button {
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tags-edit-controls button:hover {
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.save-tags-btn {
|
||||
background: var(--lora-accent) !important;
|
||||
color: white !important;
|
||||
border-color: var(--lora-accent) !important;
|
||||
}
|
||||
|
||||
.save-tags-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Add tag form */
|
||||
.add-tag-form {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
position: relative;
|
||||
width: 100%; /* Ensure full width */
|
||||
}
|
||||
|
||||
.new-tag-input {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.new-tag-input:focus {
|
||||
border-color: var(--lora-accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Tag Suggestions Dropdown Styles */
|
||||
.tag-suggestions-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
margin-top: 4px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tag-suggestions-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--card-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tag-suggestions-header span {
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tag-suggestions-header small {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tag-suggestions-container {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.tag-suggestion-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--lora-border);
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.tag-suggestion-item:hover {
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.tag-suggestion-item.already-added {
|
||||
opacity: 0.7;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tag-suggestion-item.already-added:hover {
|
||||
background: var(--lora-surface);
|
||||
border-color: var(--lora-border);
|
||||
}
|
||||
|
||||
.tag-suggestion-text {
|
||||
color: var(--lora-accent);
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 4px;
|
||||
max-width: 100px;
|
||||
}
|
||||
@@ -142,40 +142,6 @@ export function setupModelNameEditing(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存模型名称
|
||||
* @param {string} filePath - 文件路径
|
||||
*/
|
||||
async function saveModelName(filePath) {
|
||||
const modelNameElement = document.querySelector('.model-name-content');
|
||||
const newModelName = modelNameElement.textContent.trim();
|
||||
|
||||
// Validate model name
|
||||
if (!newModelName) {
|
||||
showToast('Model name cannot be empty', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if model name is too long (limit to 100 characters)
|
||||
if (newModelName.length > 100) {
|
||||
showToast('Model name is too long (maximum 100 characters)', 'error');
|
||||
// Truncate the displayed text
|
||||
modelNameElement.textContent = newModelName.substring(0, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveModelMetadata(filePath, { model_name: newModelName });
|
||||
|
||||
// Update the corresponding lora card's dataset and display
|
||||
updateLoraCard(filePath, { model_name: newModelName });
|
||||
|
||||
showToast('Model name updated successfully', 'success');
|
||||
} catch (error) {
|
||||
showToast('Failed to update model name', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基础模型编辑功能
|
||||
* @param {string} filePath - 文件路径
|
||||
|
||||
459
static/js/components/loraModal/ModelTags.js
Normal file
459
static/js/components/loraModal/ModelTags.js
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* ModelTags.js
|
||||
* Module for handling model tag editing functionality
|
||||
*/
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { saveModelMetadata } from '../../api/loraApi.js';
|
||||
import { updateLoraCard } from '../../utils/cardUpdater.js';
|
||||
|
||||
// Preset tag suggestions
|
||||
const PRESET_TAGS = [
|
||||
'character', 'style', 'concept', 'clothing',
|
||||
'poses', 'background', 'vehicle', 'buildings',
|
||||
'objects', 'animal'
|
||||
];
|
||||
|
||||
/**
|
||||
* Set up tag editing mode
|
||||
*/
|
||||
export function setupTagEditMode() {
|
||||
const editBtn = document.querySelector('.edit-tags-btn');
|
||||
if (!editBtn) return;
|
||||
|
||||
// Store original tags for restoring on cancel
|
||||
let originalTags = [];
|
||||
|
||||
editBtn.addEventListener('click', function() {
|
||||
const tagsSection = document.querySelector('.model-tags-container');
|
||||
const isEditMode = tagsSection.classList.toggle('edit-mode');
|
||||
const filePath = this.dataset.filePath;
|
||||
|
||||
// Toggle edit mode UI elements
|
||||
const compactTagsDisplay = tagsSection.querySelector('.model-tags-compact');
|
||||
const tagsEditContainer = tagsSection.querySelector('.tags-edit-container');
|
||||
|
||||
if (isEditMode) {
|
||||
// Enter edit mode
|
||||
this.innerHTML = '<i class="fas fa-times"></i>'; // Change to cancel icon
|
||||
this.title = "Cancel editing";
|
||||
|
||||
// Get all tags from tooltip, not just the visible ones in compact display
|
||||
originalTags = Array.from(
|
||||
tagsSection.querySelectorAll('.tooltip-tag')
|
||||
).map(tag => tag.textContent);
|
||||
|
||||
// Hide compact display, show edit container
|
||||
compactTagsDisplay.style.display = 'none';
|
||||
|
||||
// If edit container doesn't exist yet, create it
|
||||
if (!tagsEditContainer) {
|
||||
const editContainer = document.createElement('div');
|
||||
editContainer.className = 'tags-edit-container';
|
||||
|
||||
// Move the edit button inside the container header for better visibility
|
||||
const editBtnClone = editBtn.cloneNode(true);
|
||||
editBtnClone.classList.add('edit-header-btn');
|
||||
|
||||
// Create edit UI with edit button in the header
|
||||
editContainer.innerHTML = createTagEditUI(originalTags, editBtnClone.outerHTML);
|
||||
tagsSection.appendChild(editContainer);
|
||||
|
||||
// Setup the tag input field behavior
|
||||
setupTagInput();
|
||||
|
||||
// Create and add preset suggestions dropdown
|
||||
const tagForm = editContainer.querySelector('.add-tag-form');
|
||||
const suggestionsDropdown = createSuggestionsDropdown(originalTags);
|
||||
tagForm.appendChild(suggestionsDropdown);
|
||||
|
||||
// Setup delete buttons for existing tags
|
||||
setupDeleteButtons();
|
||||
|
||||
// Transfer click event from original button to the cloned one
|
||||
const newEditBtn = editContainer.querySelector('.edit-header-btn');
|
||||
if (newEditBtn) {
|
||||
newEditBtn.addEventListener('click', function() {
|
||||
editBtn.click();
|
||||
});
|
||||
}
|
||||
|
||||
// Hide the original button when in edit mode
|
||||
editBtn.style.display = 'none';
|
||||
} else {
|
||||
// Just show the existing edit container
|
||||
tagsEditContainer.style.display = 'block';
|
||||
editBtn.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Exit edit mode
|
||||
this.innerHTML = '<i class="fas fa-pencil-alt"></i>'; // Change back to edit icon
|
||||
this.title = "Edit tags";
|
||||
editBtn.style.display = 'block';
|
||||
|
||||
// Show compact display, hide edit container
|
||||
compactTagsDisplay.style.display = 'flex';
|
||||
if (tagsEditContainer) tagsEditContainer.style.display = 'none';
|
||||
|
||||
// Check if we're exiting edit mode due to "Save" or "Cancel"
|
||||
if (!this.dataset.skipRestore) {
|
||||
// If canceling, restore original tags
|
||||
restoreOriginalTags(tagsSection, originalTags);
|
||||
} else {
|
||||
// Reset the skip restore flag
|
||||
delete this.dataset.skipRestore;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set up save button
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('save-tags-btn') ||
|
||||
e.target.closest('.save-tags-btn')) {
|
||||
saveTags();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the tag editing UI
|
||||
* @param {Array} currentTags - Current tags
|
||||
* @param {string} editBtnHTML - HTML for the edit button to include in header
|
||||
* @returns {string} HTML markup for tag editing UI
|
||||
*/
|
||||
function createTagEditUI(currentTags, editBtnHTML = '') {
|
||||
return `
|
||||
<div class="tags-edit-content">
|
||||
<div class="tags-edit-header">
|
||||
<label>Edit Tags</label>
|
||||
${editBtnHTML}
|
||||
</div>
|
||||
<div class="tags-tags">
|
||||
${currentTags.map(tag => `
|
||||
<div class="tag-edit-tag" data-tag="${tag}">
|
||||
<span class="tag-edit-content">${tag}</span>
|
||||
<button class="delete-tag-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="tags-edit-controls">
|
||||
<button class="save-tags-btn" title="Save changes">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="add-tag-form">
|
||||
<input type="text" class="new-tag-input" placeholder="Type to add or click suggestions below">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create suggestions dropdown with preset tags
|
||||
* @param {Array} existingTags - Already added tags
|
||||
* @returns {HTMLElement} - Dropdown element
|
||||
*/
|
||||
function createSuggestionsDropdown(existingTags = []) {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'tag-suggestions-dropdown';
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'tag-suggestions-header';
|
||||
header.innerHTML = `
|
||||
<span>Suggested Tags</span>
|
||||
<small>Click to add</small>
|
||||
`;
|
||||
dropdown.appendChild(header);
|
||||
|
||||
// Create tag container
|
||||
const container = document.createElement('div');
|
||||
container.className = 'tag-suggestions-container';
|
||||
|
||||
// Add each preset tag as a suggestion
|
||||
PRESET_TAGS.forEach(tag => {
|
||||
const isAdded = existingTags.includes(tag);
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = `tag-suggestion-item ${isAdded ? 'already-added' : ''}`;
|
||||
item.title = tag;
|
||||
item.innerHTML = `
|
||||
<span class="tag-suggestion-text">${tag}</span>
|
||||
${isAdded ? '<span class="added-indicator"><i class="fas fa-check"></i></span>' : ''}
|
||||
`;
|
||||
|
||||
if (!isAdded) {
|
||||
item.addEventListener('click', () => {
|
||||
addNewTag(tag);
|
||||
|
||||
// Also populate the input field for potential editing
|
||||
const input = document.querySelector('.new-tag-input');
|
||||
if (input) input.value = tag;
|
||||
|
||||
// Focus on the input
|
||||
if (input) input.focus();
|
||||
|
||||
// Update dropdown without removing it
|
||||
updateSuggestionsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up tag input behavior
|
||||
*/
|
||||
function setupTagInput() {
|
||||
const tagInput = document.querySelector('.new-tag-input');
|
||||
|
||||
if (tagInput) {
|
||||
tagInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewTag(this.value);
|
||||
this.value = ''; // Clear input after adding
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up delete buttons for tags
|
||||
*/
|
||||
function setupDeleteButtons() {
|
||||
document.querySelectorAll('.delete-tag-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const tag = this.closest('.tag-edit-tag');
|
||||
tag.remove();
|
||||
|
||||
// Update status of items in the suggestion dropdown
|
||||
updateSuggestionsDropdown();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new tag
|
||||
* @param {string} tag - Tag to add
|
||||
*/
|
||||
function addNewTag(tag) {
|
||||
tag = tag.trim().toLowerCase();
|
||||
if (!tag) return;
|
||||
|
||||
const tagsContainer = document.querySelector('.tags-tags');
|
||||
if (!tagsContainer) return;
|
||||
|
||||
// Validation: Check length
|
||||
if (tag.length > 30) {
|
||||
showToast('Tag should not exceed 30 characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation: Check total number
|
||||
const currentTags = tagsContainer.querySelectorAll('.tag-edit-tag');
|
||||
if (currentTags.length >= 30) {
|
||||
showToast('Maximum 30 tags allowed', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation: Check for duplicates
|
||||
const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||
if (existingTags.includes(tag)) {
|
||||
showToast('This tag already exists', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new tag
|
||||
const newTag = document.createElement('div');
|
||||
newTag.className = 'tag-edit-tag';
|
||||
newTag.dataset.tag = tag;
|
||||
newTag.innerHTML = `
|
||||
<span class="tag-edit-content">${tag}</span>
|
||||
<button class="delete-tag-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add event listener to delete button
|
||||
const deleteBtn = newTag.querySelector('.delete-tag-btn');
|
||||
deleteBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
newTag.remove();
|
||||
|
||||
// Update status of items in the suggestion dropdown
|
||||
updateSuggestionsDropdown();
|
||||
});
|
||||
|
||||
tagsContainer.appendChild(newTag);
|
||||
|
||||
// Update status of items in the suggestions dropdown
|
||||
updateSuggestionsDropdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status of items in the suggestions dropdown
|
||||
*/
|
||||
function updateSuggestionsDropdown() {
|
||||
const dropdown = document.querySelector('.tag-suggestions-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
// Get all current tags
|
||||
const currentTags = document.querySelectorAll('.tag-edit-tag');
|
||||
const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||
|
||||
// Update status of each item in dropdown
|
||||
dropdown.querySelectorAll('.tag-suggestion-item').forEach(item => {
|
||||
const tagText = item.querySelector('.tag-suggestion-text').textContent;
|
||||
const isAdded = existingTags.includes(tagText);
|
||||
|
||||
if (isAdded) {
|
||||
item.classList.add('already-added');
|
||||
|
||||
// Add indicator if it doesn't exist
|
||||
let indicator = item.querySelector('.added-indicator');
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('span');
|
||||
indicator.className = 'added-indicator';
|
||||
indicator.innerHTML = '<i class="fas fa-check"></i>';
|
||||
item.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 tag = item.querySelector('.tag-suggestion-text').textContent;
|
||||
addNewTag(tag);
|
||||
|
||||
// Also populate the input field
|
||||
const input = document.querySelector('.new-tag-input');
|
||||
if (input) input.value = tag;
|
||||
|
||||
// Focus the input
|
||||
if (input) input.focus();
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore original tags when canceling edit
|
||||
* @param {HTMLElement} section - The tags section
|
||||
* @param {Array} originalTags - Original tags array
|
||||
*/
|
||||
function restoreOriginalTags(section, originalTags) {
|
||||
// Nothing to do here as we're just hiding the edit UI
|
||||
// and showing the original compact tags which weren't modified
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tags
|
||||
*/
|
||||
async function saveTags() {
|
||||
const editBtn = document.querySelector('.edit-tags-btn');
|
||||
if (!editBtn) return;
|
||||
|
||||
const filePath = editBtn.dataset.filePath;
|
||||
const tagElements = document.querySelectorAll('.tag-edit-tag');
|
||||
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||
|
||||
// Get original tags to compare
|
||||
const originalTagElements = document.querySelectorAll('.tooltip-tag');
|
||||
const originalTags = Array.from(originalTagElements).map(tag => tag.textContent);
|
||||
|
||||
// Check if tags have actually changed
|
||||
const tagsChanged = JSON.stringify(tags) !== JSON.stringify(originalTags);
|
||||
|
||||
if (!tagsChanged) {
|
||||
// No changes made, just exit edit mode without API call
|
||||
editBtn.dataset.skipRestore = "true";
|
||||
editBtn.click();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Save tags metadata
|
||||
await saveModelMetadata(filePath, { tags: tags });
|
||||
|
||||
// Set flag to skip restoring original tags when exiting edit mode
|
||||
editBtn.dataset.skipRestore = "true";
|
||||
|
||||
// Update the compact tags display
|
||||
const compactTagsContainer = document.querySelector('.model-tags-container');
|
||||
if (compactTagsContainer) {
|
||||
// Generate new compact tags HTML
|
||||
const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact');
|
||||
|
||||
if (compactTagsDisplay) {
|
||||
// Clear current tags
|
||||
compactTagsDisplay.innerHTML = '';
|
||||
|
||||
// Add visible tags (up to 5)
|
||||
const visibleTags = tags.slice(0, 5);
|
||||
visibleTags.forEach(tag => {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'model-tag-compact';
|
||||
span.textContent = tag;
|
||||
compactTagsDisplay.appendChild(span);
|
||||
});
|
||||
|
||||
// Add more indicator if needed
|
||||
const remainingCount = Math.max(0, tags.length - 5);
|
||||
if (remainingCount > 0) {
|
||||
const more = document.createElement('span');
|
||||
more.className = 'model-tag-more';
|
||||
more.dataset.count = remainingCount;
|
||||
more.textContent = `+${remainingCount}`;
|
||||
compactTagsDisplay.appendChild(more);
|
||||
}
|
||||
}
|
||||
|
||||
// Update tooltip content
|
||||
const tooltipContent = compactTagsContainer.querySelector('.tooltip-content');
|
||||
if (tooltipContent) {
|
||||
tooltipContent.innerHTML = '';
|
||||
|
||||
tags.forEach(tag => {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'tooltip-tag';
|
||||
span.textContent = tag;
|
||||
tooltipContent.appendChild(span);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exit edit mode
|
||||
editBtn.click();
|
||||
|
||||
// Update the LoRA card's dataset
|
||||
const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
if (loraCard) {
|
||||
loraCard.dataset.tags = JSON.stringify(tags);
|
||||
|
||||
// Also update the card in the DOM
|
||||
// updateLoraCard(loraCard, { tags: tags });
|
||||
}
|
||||
|
||||
showToast('Tags updated successfully', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error saving tags:', error);
|
||||
showToast('Failed to update tags', 'error');
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import { renderShowcaseContent, toggleShowcase, setupShowcaseScroll, scrollToTop
|
||||
import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
|
||||
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
||||
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
||||
import { loadRecipesForLora } from './RecipeTab.js'; // Add import for recipe tab
|
||||
import { loadRecipesForLora } from './RecipeTab.js';
|
||||
import { setupTagEditMode } from './ModelTags.js'; // Add import for tag editing
|
||||
import {
|
||||
setupModelNameEditing,
|
||||
setupBaseModelEditing,
|
||||
@@ -52,7 +53,7 @@ export function showLoraModal(lora) {
|
||||
<span class="creator-username">${lora.civitai.creator.username}</span>
|
||||
</div>` : ''}
|
||||
|
||||
${renderCompactTags(lora.tags || [])}
|
||||
${renderCompactTags(lora.tags || [], lora.file_path)}
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
@@ -177,6 +178,7 @@ export function showLoraModal(lora) {
|
||||
setupModelNameEditing(lora.file_path);
|
||||
setupBaseModelEditing(lora.file_path);
|
||||
setupFileNameEditing(lora.file_path);
|
||||
setupTagEditMode(); // Initialize tag editing functionality
|
||||
|
||||
// If we have a model ID but no description, fetch it
|
||||
if (lora.civitai?.modelId && !lora.modelDescription) {
|
||||
|
||||
@@ -26,9 +26,10 @@ export function formatFileSize(bytes) {
|
||||
/**
|
||||
* 渲染紧凑标签
|
||||
* @param {Array} tags - 标签数组
|
||||
* @param {string} filePath - 文件路径,用于编辑按钮
|
||||
* @returns {string} HTML内容
|
||||
*/
|
||||
export function renderCompactTags(tags) {
|
||||
export function renderCompactTags(tags, filePath = '') {
|
||||
if (!tags || tags.length === 0) return '';
|
||||
|
||||
// Display up to 5 tags, with a tooltip indicator if there are more
|
||||
@@ -37,11 +38,16 @@ export function renderCompactTags(tags) {
|
||||
|
||||
return `
|
||||
<div class="model-tags-container">
|
||||
<div class="model-tags-compact">
|
||||
${visibleTags.map(tag => `<span class="model-tag-compact">${tag}</span>`).join('')}
|
||||
${remainingCount > 0 ?
|
||||
`<span class="model-tag-more" data-count="${remainingCount}">+${remainingCount}</span>` :
|
||||
''}
|
||||
<div class="model-tags-header">
|
||||
<div class="model-tags-compact">
|
||||
${visibleTags.map(tag => `<span class="model-tag-compact">${tag}</span>`).join('')}
|
||||
${remainingCount > 0 ?
|
||||
`<span class="model-tag-more" data-count="${remainingCount}">+${remainingCount}</span>` :
|
||||
''}
|
||||
</div>
|
||||
<button class="edit-tags-btn" data-file-path="${filePath}" title="Edit tags">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
${tags.length > 0 ?
|
||||
`<div class="model-tags-tooltip">
|
||||
|
||||
Reference in New Issue
Block a user