mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-19 17:02:04 -03:00
feat(tags): unify recipe modal tag UI with model modal
- Replace recipe modal's custom tag display/edit with shared renderCompactTags/setupTagEditMode from ModelTags and utils - Remove 300+ lines of duplicated tag display and editing code - Parameterize setupTagEditMode with saveHandler/onSaved/showSuggestions options for recipe-specific save flow (updateRecipeMetadata + dirty state) - Scope all DOM queries in ModelTags.js via options.container / this.closest to prevent cross-modal element conflicts - Fix edit button alignment (justify-content: flex-start) - Fix tag tooltip selector scoping in setupTagTooltip - Add width: 100% to #recipeTagsContainer for edit container full width
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.model-tag-compact {
|
||||
@@ -28,6 +30,9 @@
|
||||
font-size: 0.75em;
|
||||
color: var(--text-color);
|
||||
white-space: nowrap;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Style for empty tags placeholder */
|
||||
@@ -118,8 +123,9 @@
|
||||
/* Model Tags Edit Mode */
|
||||
.model-tags-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.edit-tags-btn {
|
||||
@@ -132,6 +138,7 @@
|
||||
border-radius: var(--border-radius-xs);
|
||||
transition: var(--transition-base);
|
||||
margin-left: var(--space-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-tags-btn.visible,
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#recipeTagsContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recipe-modal-header h2 {
|
||||
margin: 0 0 var(--space-1);
|
||||
padding: var(--space-1);
|
||||
@@ -95,127 +99,11 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-editor.tags-editor input {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Remove obsolete button styles */
|
||||
.editor-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Special styling for tags content */
|
||||
.tags-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tags-display {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.no-tags {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Recipe Tags styles */
|
||||
.recipe-tags-container {
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.recipe-tags-compact {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recipe-tag-compact {
|
||||
background: var(--surface-subtle);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75em;
|
||||
color: var(--text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .recipe-tag-compact {
|
||||
background: var(--surface-subtle);
|
||||
border: 1px solid var(--lora-border);
|
||||
}
|
||||
|
||||
.recipe-tag-more {
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75em;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recipe-tags-tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
padding: 10px 14px;
|
||||
max-width: 400px;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-4px);
|
||||
transition: var(--transition-base);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.recipe-tags-tooltip.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tooltip-tag {
|
||||
background: var(--surface-hover);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 3px 8px;
|
||||
font-size: 0.75em;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tooltip-tag {
|
||||
background: var(--surface-hover);
|
||||
border: 1px solid var(--lora-border);
|
||||
}
|
||||
|
||||
#recipeModal .modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1153,7 +1041,7 @@
|
||||
max-height: 2.4em;
|
||||
}
|
||||
|
||||
.recipe-tags-container {
|
||||
#recipeTagsContainer {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { fetchRecipeDetails, updateRecipeMetadata } from '../api/recipeApi.js';
|
||||
import { downloadManager } from '../managers/DownloadManager.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
import { openMediaViewer } from './shared/MediaViewer.js';
|
||||
import { renderCompactTags, setupTagTooltip } from './shared/utils.js';
|
||||
import { setupTagEditMode } from './shared/ModelTags.js';
|
||||
|
||||
const ALLOWED_GEN_PARAM_KEYS = new Set([
|
||||
'prompt',
|
||||
@@ -139,14 +141,6 @@ class RecipeModal {
|
||||
this.saveTitleEdit();
|
||||
}
|
||||
|
||||
// Handle tags edit
|
||||
const tagsEditor = document.getElementById('recipeTagsEditor');
|
||||
if (tagsEditor && tagsEditor.classList.contains('active') &&
|
||||
!tagsEditor.contains(event.target) &&
|
||||
!event.target.closest('.edit-icon')) {
|
||||
this.saveTagsEdit();
|
||||
}
|
||||
|
||||
// Handle reconnect input
|
||||
const reconnectContainers = document.querySelectorAll('.lora-reconnect-container');
|
||||
reconnectContainers.forEach(container => {
|
||||
@@ -236,98 +230,10 @@ class RecipeModal {
|
||||
this.filePath = hydratedRecipe.file_path;
|
||||
this.listFilePath = hydratedRecipe.file_path;
|
||||
|
||||
// Set recipe tags if they exist
|
||||
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
||||
const tagsTooltipContent = document.getElementById('recipeTagsTooltipContent');
|
||||
|
||||
if (tagsCompactElement) {
|
||||
// Add tags container with edit functionality
|
||||
tagsCompactElement.innerHTML = `
|
||||
<div class="editable-content tags-content">
|
||||
<div class="tags-display"></div>
|
||||
<button class="edit-icon" title="Edit tags"><i class="fas fa-pencil-alt"></i></button>
|
||||
</div>
|
||||
<div id="recipeTagsEditor" class="content-editor tags-editor">
|
||||
<input type="text" class="tags-input" placeholder="Enter tags separated by commas">
|
||||
</div>
|
||||
`;
|
||||
|
||||
const tagsDisplay = tagsCompactElement.querySelector('.tags-display');
|
||||
|
||||
if (hydratedRecipe.tags && hydratedRecipe.tags.length > 0) {
|
||||
// Limit displayed tags to 5, show a "+X more" button if needed
|
||||
const maxVisibleTags = 5;
|
||||
const visibleTags = hydratedRecipe.tags.slice(0, maxVisibleTags);
|
||||
const remainingTags = hydratedRecipe.tags.length > maxVisibleTags ? hydratedRecipe.tags.slice(maxVisibleTags) : [];
|
||||
|
||||
// Add visible tags
|
||||
visibleTags.forEach(tag => {
|
||||
const tagElement = document.createElement('div');
|
||||
tagElement.className = 'recipe-tag-compact';
|
||||
tagElement.textContent = tag;
|
||||
tagsDisplay.appendChild(tagElement);
|
||||
});
|
||||
|
||||
// Add "more" button if needed
|
||||
if (remainingTags.length > 0) {
|
||||
const moreButton = document.createElement('div');
|
||||
moreButton.className = 'recipe-tag-more';
|
||||
moreButton.textContent = `+${remainingTags.length} more`;
|
||||
tagsDisplay.appendChild(moreButton);
|
||||
|
||||
// Add tooltip functionality
|
||||
moreButton.addEventListener('mouseenter', () => {
|
||||
document.getElementById('recipeTagsTooltip').classList.add('visible');
|
||||
});
|
||||
|
||||
moreButton.addEventListener('mouseleave', () => {
|
||||
setTimeout(() => {
|
||||
if (!document.getElementById('recipeTagsTooltip').matches(':hover')) {
|
||||
document.getElementById('recipeTagsTooltip').classList.remove('visible');
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.getElementById('recipeTagsTooltip').addEventListener('mouseleave', () => {
|
||||
document.getElementById('recipeTagsTooltip').classList.remove('visible');
|
||||
});
|
||||
|
||||
// Add all tags to tooltip
|
||||
if (tagsTooltipContent) {
|
||||
tagsTooltipContent.innerHTML = '';
|
||||
hydratedRecipe.tags.forEach(tag => {
|
||||
const tooltipTag = document.createElement('div');
|
||||
tooltipTag.className = 'tooltip-tag';
|
||||
tooltipTag.textContent = tag;
|
||||
tagsTooltipContent.appendChild(tooltipTag);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tagsDisplay.innerHTML = '<div class="no-tags">No tags</div>';
|
||||
}
|
||||
|
||||
// Add event listeners for tags editing
|
||||
const editTagsIcon = tagsCompactElement.querySelector('.edit-icon');
|
||||
const tagsInput = tagsCompactElement.querySelector('.tags-input');
|
||||
|
||||
// Set current tags in the input
|
||||
if (hydratedRecipe.tags && hydratedRecipe.tags.length > 0) {
|
||||
tagsInput.value = hydratedRecipe.tags.join(', ');
|
||||
}
|
||||
|
||||
editTagsIcon.addEventListener('click', () => this.showTagsEditor());
|
||||
|
||||
// Add key event listener for Enter key
|
||||
tagsInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.saveTagsEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this.cancelTagsEdit();
|
||||
}
|
||||
});
|
||||
// Render tags using shared utility
|
||||
const tagsContainer = document.getElementById('recipeTagsContainer');
|
||||
if (tagsContainer) {
|
||||
this.updateTagsDisplay(tagsContainer, hydratedRecipe.tags || []);
|
||||
}
|
||||
|
||||
// Set recipe image
|
||||
@@ -609,17 +515,35 @@ class RecipeModal {
|
||||
}
|
||||
|
||||
syncTagsDisplay(tags) {
|
||||
const tagsContainer = document.getElementById('recipeTagsCompact');
|
||||
if (!tagsContainer) {
|
||||
return;
|
||||
const container = document.getElementById('recipeTagsContainer');
|
||||
if (!container) return;
|
||||
this.updateTagsDisplay(container, tags || []);
|
||||
}
|
||||
|
||||
this.updateTagsDisplay(tagsContainer, tags || []);
|
||||
// Re-render tags display using shared utility, wire edit mode with ModelTags
|
||||
updateTagsDisplay(container, tags) {
|
||||
const filePath = this.filePath || '';
|
||||
|
||||
const tagsInput = tagsContainer.querySelector('.tags-input');
|
||||
if (tagsInput) {
|
||||
tagsInput.value = tags && tags.length > 0 ? tags.join(', ') : '';
|
||||
}
|
||||
container.innerHTML = renderCompactTags(tags, filePath);
|
||||
|
||||
// Setup tooltip for all tags
|
||||
setupTagTooltip(container);
|
||||
|
||||
// Wire edit button using shared tag editing (no suggestions for recipes)
|
||||
setupTagEditMode(null, {
|
||||
container: container,
|
||||
showSuggestions: false,
|
||||
normalizeTag: false,
|
||||
saveHandler: async (filePath, tags) => {
|
||||
await updateRecipeMetadata(filePath, { tags }, this.getMetadataUpdateOptions());
|
||||
},
|
||||
onSaved: (tags) => {
|
||||
this.currentRecipe.tags = tags;
|
||||
this.commitField('tags');
|
||||
const c = document.getElementById('recipeTagsContainer');
|
||||
if (c) this.updateTagsDisplay(c, tags);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
syncPromptField(field, value, placeholder) {
|
||||
@@ -976,139 +900,6 @@ class RecipeModal {
|
||||
}
|
||||
}
|
||||
|
||||
// Tags editing methods
|
||||
showTagsEditor() {
|
||||
const tagsContainer = document.getElementById('recipeTagsCompact');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.querySelector('.editable-content').classList.add('hide');
|
||||
const editor = tagsContainer.querySelector('#recipeTagsEditor');
|
||||
editor.classList.add('active');
|
||||
const input = editor.querySelector('input');
|
||||
input.oninput = () => this.markFieldDirty('tags');
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
saveTagsEdit() {
|
||||
const tagsContainer = document.getElementById('recipeTagsCompact');
|
||||
if (tagsContainer) {
|
||||
const editor = tagsContainer.querySelector('#recipeTagsEditor');
|
||||
const input = editor.querySelector('input');
|
||||
const tagsText = input.value.trim();
|
||||
|
||||
// Parse tags
|
||||
let newTags = [];
|
||||
if (tagsText) {
|
||||
newTags = tagsText.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag.length > 0);
|
||||
}
|
||||
|
||||
// Check if tags changed
|
||||
const oldTags = this.currentRecipe.tags || [];
|
||||
const tagsChanged =
|
||||
newTags.length !== oldTags.length ||
|
||||
newTags.some((tag, index) => tag !== oldTags[index]);
|
||||
|
||||
if (tagsChanged) {
|
||||
// Update the recipe on the server
|
||||
updateRecipeMetadata(this.filePath, { tags: newTags }, this.getMetadataUpdateOptions())
|
||||
.then(data => {
|
||||
// Show success toast
|
||||
showToast('toast.recipes.tagsUpdated', {}, 'success');
|
||||
|
||||
// Update the current recipe object
|
||||
this.currentRecipe.tags = newTags;
|
||||
this.commitField('tags');
|
||||
|
||||
// Update tags in the UI
|
||||
this.updateTagsDisplay(tagsContainer, newTags);
|
||||
})
|
||||
.catch(error => {
|
||||
// Error is handled in the API function
|
||||
this.clearFieldDirty('tags');
|
||||
});
|
||||
} else {
|
||||
this.clearFieldDirty('tags');
|
||||
}
|
||||
|
||||
// Hide editor
|
||||
editor.classList.remove('active');
|
||||
tagsContainer.querySelector('.editable-content').classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to update tags display
|
||||
updateTagsDisplay(tagsContainer, tags) {
|
||||
const tagsDisplay = tagsContainer.querySelector('.tags-display');
|
||||
tagsDisplay.innerHTML = '';
|
||||
|
||||
if (tags.length > 0) {
|
||||
// Limit displayed tags to 5, show a "+X more" button if needed
|
||||
const maxVisibleTags = 5;
|
||||
const visibleTags = tags.slice(0, maxVisibleTags);
|
||||
const remainingTags = tags.length > maxVisibleTags ? tags.slice(maxVisibleTags) : [];
|
||||
|
||||
// Add visible tags
|
||||
visibleTags.forEach(tag => {
|
||||
const tagElement = document.createElement('div');
|
||||
tagElement.className = 'recipe-tag-compact';
|
||||
tagElement.textContent = tag;
|
||||
tagsDisplay.appendChild(tagElement);
|
||||
});
|
||||
|
||||
// Add "more" button if needed
|
||||
if (remainingTags.length > 0) {
|
||||
const moreButton = document.createElement('div');
|
||||
moreButton.className = 'recipe-tag-more';
|
||||
moreButton.textContent = `+${remainingTags.length} more`;
|
||||
tagsDisplay.appendChild(moreButton);
|
||||
|
||||
// Update tooltip content
|
||||
const tooltipContent = document.getElementById('recipeTagsTooltipContent');
|
||||
if (tooltipContent) {
|
||||
tooltipContent.innerHTML = '';
|
||||
tags.forEach(tag => {
|
||||
const tooltipTag = document.createElement('div');
|
||||
tooltipTag.className = 'tooltip-tag';
|
||||
tooltipTag.textContent = tag;
|
||||
tooltipContent.appendChild(tooltipTag);
|
||||
});
|
||||
}
|
||||
|
||||
// Re-add tooltip functionality
|
||||
moreButton.addEventListener('mouseenter', () => {
|
||||
document.getElementById('recipeTagsTooltip').classList.add('visible');
|
||||
});
|
||||
|
||||
moreButton.addEventListener('mouseleave', () => {
|
||||
setTimeout(() => {
|
||||
if (!document.getElementById('recipeTagsTooltip').matches(':hover')) {
|
||||
document.getElementById('recipeTagsTooltip').classList.remove('visible');
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
tagsDisplay.innerHTML = '<div class="no-tags">No tags</div>';
|
||||
}
|
||||
}
|
||||
|
||||
cancelTagsEdit() {
|
||||
const tagsContainer = document.getElementById('recipeTagsCompact');
|
||||
if (tagsContainer) {
|
||||
// Reset input value
|
||||
const editor = tagsContainer.querySelector('#recipeTagsEditor');
|
||||
const input = editor.querySelector('input');
|
||||
input.value = this.currentRecipe.tags ? this.currentRecipe.tags.join(', ') : '';
|
||||
this.clearFieldDirty('tags');
|
||||
|
||||
// Hide editor
|
||||
editor.classList.remove('active');
|
||||
tagsContainer.querySelector('.editable-content').classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
setupPromptEditors() {
|
||||
const promptConfigs = [
|
||||
{
|
||||
|
||||
@@ -29,6 +29,14 @@ let priorityTagSuggestionsLoaded = false;
|
||||
let priorityTagSuggestionsPromise = null;
|
||||
let activeTagDragState = null;
|
||||
|
||||
// Configurable options for tag editing (set by setupTagEditMode)
|
||||
let tagEditOptions = {
|
||||
showSuggestions: true,
|
||||
saveHandler: null,
|
||||
onSaved: null,
|
||||
normalizeTag: true,
|
||||
};
|
||||
|
||||
function normalizeModelTypeKey(modelType) {
|
||||
if (!modelType) {
|
||||
return '';
|
||||
@@ -140,13 +148,30 @@ let saveTagsHandler = null;
|
||||
|
||||
/**
|
||||
* Set up tag editing mode
|
||||
* @param {string|null} modelType - Model type for suggestions (e.g. 'loras', 'checkpoints')
|
||||
* @param {Object} [options] - Optional configuration
|
||||
* @param {boolean} [options.showSuggestions=true] - Show priority tag suggestions dropdown
|
||||
* @param {Function} [options.saveHandler] - Custom save function, async (filePath, tags) => {}
|
||||
* @param {Function} [options.onSaved] - Called after successful save, (tags) => {}
|
||||
* @param {boolean} [options.normalizeTag=true] - Lowercase tag on add
|
||||
*/
|
||||
export function setupTagEditMode(modelType = null) {
|
||||
const editBtn = document.querySelector('.edit-tags-btn');
|
||||
export function setupTagEditMode(modelType = null, options = {}) {
|
||||
// Store options for use by saveTags and addNewTag
|
||||
tagEditOptions = {
|
||||
showSuggestions: options.showSuggestions !== false,
|
||||
saveHandler: options.saveHandler || null,
|
||||
onSaved: options.onSaved || null,
|
||||
normalizeTag: options.normalizeTag !== false,
|
||||
};
|
||||
|
||||
const root = options.container || document;
|
||||
const editBtn = root.querySelector('.edit-tags-btn');
|
||||
if (!editBtn) return;
|
||||
|
||||
if (tagEditOptions.showSuggestions) {
|
||||
setActiveModelTypeKey(modelType);
|
||||
ensurePriorityTagSuggestions();
|
||||
}
|
||||
|
||||
// Store original tags for restoring on cancel
|
||||
let originalTags = [];
|
||||
@@ -158,7 +183,8 @@ export function setupTagEditMode(modelType = null) {
|
||||
|
||||
// Create new handler and store reference
|
||||
const editBtnClickHandler = function() {
|
||||
const tagsSection = document.querySelector('.model-tags-container');
|
||||
const tagsSection = this.closest('.model-tags-container');
|
||||
if (!tagsSection) return;
|
||||
const isEditMode = tagsSection.classList.toggle('edit-mode');
|
||||
const filePath = this.dataset.filePath;
|
||||
|
||||
@@ -193,16 +219,18 @@ export function setupTagEditMode(modelType = null) {
|
||||
tagsSection.appendChild(editContainer);
|
||||
|
||||
// Setup the tag input field behavior
|
||||
setupTagInput();
|
||||
setupTagInput(tagsSection);
|
||||
|
||||
// Create and add preset suggestions dropdown
|
||||
if (tagEditOptions.showSuggestions) {
|
||||
const tagForm = editContainer.querySelector('.metadata-add-form');
|
||||
const suggestionsDropdown = createSuggestionsDropdown(originalTags);
|
||||
tagForm.appendChild(suggestionsDropdown);
|
||||
}
|
||||
|
||||
// Setup delete buttons for existing tags
|
||||
setupDeleteButtons();
|
||||
setupTagDragAndDrop();
|
||||
setupTagDragAndDrop(tagsSection);
|
||||
|
||||
// Transfer click event from original button to the cloned one
|
||||
const newEditBtn = editContainer.querySelector('.metadata-header-btn');
|
||||
@@ -218,7 +246,7 @@ export function setupTagEditMode(modelType = null) {
|
||||
// Just show the existing edit container
|
||||
tagsEditContainer.style.display = 'block';
|
||||
editBtn.style.display = 'none';
|
||||
setupTagDragAndDrop();
|
||||
setupTagDragAndDrop(tagsSection);
|
||||
}
|
||||
} else {
|
||||
// Exit edit mode
|
||||
@@ -255,7 +283,7 @@ export function setupTagEditMode(modelType = null) {
|
||||
saveTagsHandler = function(e) {
|
||||
if (e.target.classList.contains('save-tags-btn') ||
|
||||
e.target.closest('.save-tags-btn')) {
|
||||
saveTags();
|
||||
saveTags(e.target);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -267,19 +295,28 @@ export function setupTagEditMode(modelType = null) {
|
||||
|
||||
/**
|
||||
* Save tags
|
||||
* @param {Element} [triggerElement] - The element that triggered the save (e.g. save button)
|
||||
*/
|
||||
async function saveTags() {
|
||||
const editBtn = document.querySelector('.edit-tags-btn');
|
||||
if (!editBtn) return;
|
||||
async function saveTags(triggerElement = null) {
|
||||
let editBtn;
|
||||
let scope;
|
||||
if (triggerElement) {
|
||||
scope = triggerElement.closest('.model-tags-container');
|
||||
editBtn = scope ? scope.querySelector('.edit-tags-btn') : document.querySelector('.edit-tags-btn');
|
||||
} else {
|
||||
scope = document.querySelector('.model-tags-container');
|
||||
editBtn = scope ? scope.querySelector('.edit-tags-btn') : null;
|
||||
}
|
||||
if (!editBtn || !scope) return;
|
||||
|
||||
const filePath = editBtn.dataset.filePath;
|
||||
const tagElements = document.querySelectorAll('.metadata-item');
|
||||
const tagElements = scope.querySelectorAll('.metadata-item');
|
||||
let tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||
|
||||
// Flush uncommitted input as a tag so it's not silently lost on save
|
||||
const tagInput = document.querySelector('.metadata-input');
|
||||
const tagInput = scope.querySelector('.metadata-input');
|
||||
if (tagInput) {
|
||||
const pendingTag = tagInput.value.trim().toLowerCase();
|
||||
const pendingTag = tagEditOptions.normalizeTag ? tagInput.value.trim().toLowerCase() : tagInput.value.trim();
|
||||
if (pendingTag && !tags.includes(pendingTag)) {
|
||||
tags.push(pendingTag);
|
||||
}
|
||||
@@ -287,7 +324,7 @@ async function saveTags() {
|
||||
}
|
||||
|
||||
// Get original tags to compare
|
||||
const originalTagElements = document.querySelectorAll('.tooltip-tag');
|
||||
const originalTagElements = scope.querySelectorAll('.tooltip-tag');
|
||||
const originalTags = Array.from(originalTagElements).map(tag => tag.textContent);
|
||||
|
||||
// Check if tags have actually changed
|
||||
@@ -301,14 +338,22 @@ async function saveTags() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Save tags metadata
|
||||
// Use custom save handler if provided, otherwise default model API
|
||||
if (tagEditOptions.saveHandler) {
|
||||
await tagEditOptions.saveHandler(filePath, tags);
|
||||
} else {
|
||||
await getModelApiClient().saveModelMetadata(filePath, { tags: tags });
|
||||
}
|
||||
|
||||
// Set flag to skip restoring original tags when exiting edit mode
|
||||
editBtn.dataset.skipRestore = "true";
|
||||
|
||||
// Use custom onSaved if provided (e.g. for recipe dirty state + re-render)
|
||||
if (tagEditOptions.onSaved) {
|
||||
tagEditOptions.onSaved(tags);
|
||||
} else {
|
||||
// Update the compact tags display
|
||||
const compactTagsContainer = document.querySelector('.model-tags-container');
|
||||
const compactTagsContainer = scope;
|
||||
if (compactTagsContainer) {
|
||||
// Generate new compact tags HTML
|
||||
const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact');
|
||||
@@ -353,6 +398,7 @@ async function saveTags() {
|
||||
|
||||
// Exit edit mode
|
||||
editBtn.click();
|
||||
}
|
||||
|
||||
showToast('modelTags.messages.updated', {}, 'success');
|
||||
} catch (error) {
|
||||
@@ -470,16 +516,19 @@ function renderPriorityTagSuggestions(container, existingTags = []) {
|
||||
|
||||
/**
|
||||
* Set up tag input behavior
|
||||
* @param {Element} scopeContainer - The .model-tags-container element
|
||||
*/
|
||||
function setupTagInput() {
|
||||
const tagInput = document.querySelector('.metadata-input');
|
||||
function setupTagInput(scopeContainer) {
|
||||
const tagInput = scopeContainer
|
||||
? scopeContainer.querySelector('.metadata-input')
|
||||
: document.querySelector('.metadata-input');
|
||||
|
||||
if (tagInput) {
|
||||
tagInput.focus();
|
||||
tagInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewTag(this.value);
|
||||
addNewTag(this.value, this);
|
||||
this.value = ''; // Clear input after adding
|
||||
}
|
||||
});
|
||||
@@ -504,9 +553,12 @@ function setupDeleteButtons() {
|
||||
|
||||
/**
|
||||
* Enable drag-and-drop sorting for tag items
|
||||
* @param {Element} [scopeContainer] - Optional scoped .model-tags-container element
|
||||
*/
|
||||
function setupTagDragAndDrop() {
|
||||
const container = document.querySelector(METADATA_ITEMS_CONTAINER_SELECTOR);
|
||||
function setupTagDragAndDrop(scopeContainer) {
|
||||
const container = scopeContainer
|
||||
? scopeContainer.querySelector(METADATA_ITEMS_CONTAINER_SELECTOR)
|
||||
: document.querySelector(METADATA_ITEMS_CONTAINER_SELECTOR);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
@@ -712,12 +764,14 @@ function finishPointerDrag() {
|
||||
/**
|
||||
* Add a new tag
|
||||
* @param {string} tag - Tag to add
|
||||
* @param {Element} [scopeElement] - Element within the correct .model-tags-container for scoping
|
||||
*/
|
||||
function addNewTag(tag) {
|
||||
tag = tag.trim().toLowerCase();
|
||||
function addNewTag(tag, scopeElement = null) {
|
||||
tag = tagEditOptions.normalizeTag ? tag.trim().toLowerCase() : tag.trim();
|
||||
if (!tag) return;
|
||||
|
||||
const tagsContainer = document.querySelector('.metadata-items');
|
||||
const scope = scopeElement ? scopeElement.closest('.model-tags-container') : document;
|
||||
const tagsContainer = scope.querySelector('.metadata-items');
|
||||
if (!tagsContainer) return;
|
||||
|
||||
// Validation: Check length
|
||||
@@ -762,7 +816,7 @@ function addNewTag(tag) {
|
||||
});
|
||||
|
||||
tagsContainer.appendChild(newTag);
|
||||
setupTagDragAndDrop();
|
||||
setupTagDragAndDrop(scope);
|
||||
|
||||
// Update status of items in the suggestions dropdown
|
||||
updateSuggestionsDropdown();
|
||||
|
||||
@@ -78,10 +78,12 @@ export function renderCompactTags(tags, filePath = '') {
|
||||
|
||||
/**
|
||||
* Set up tag tooltip functionality
|
||||
* @param {Element} [scopeContainer] - Optional container to scope the querySelector
|
||||
*/
|
||||
export function setupTagTooltip() {
|
||||
const tagsContainer = document.querySelector('.model-tags-container');
|
||||
const tooltip = document.querySelector('.model-tags-tooltip');
|
||||
export function setupTagTooltip(scopeContainer = null) {
|
||||
const root = scopeContainer || document;
|
||||
const tagsContainer = root.querySelector('.model-tags-container');
|
||||
const tooltip = root.querySelector('.model-tags-tooltip');
|
||||
|
||||
if (tagsContainer && tooltip) {
|
||||
tagsContainer.addEventListener('mouseenter', () => {
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<!-- Header Actions: populated dynamically in RecipeModal.js -->
|
||||
<div class="recipe-header-actions" id="recipeHeaderActions"></div>
|
||||
<!-- Recipe Tags Container -->
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recipe Tags Container (rendered by renderCompactTags) -->
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
@@ -246,12 +246,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -375,12 +370,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -474,12 +464,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -588,12 +573,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -682,12 +662,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -790,12 +765,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -873,12 +843,10 @@ describe('Interaction-level regression coverage', () => {
|
||||
});
|
||||
|
||||
recipeModal.markFieldDirty('title');
|
||||
recipeModal.markFieldDirty('tags');
|
||||
recipeModal.markFieldDirty('prompt');
|
||||
recipeModal.markFieldDirty('negative_prompt');
|
||||
|
||||
document.querySelector('#recipeTitleEditor .title-input').value = 'Local Title';
|
||||
document.querySelector('#recipeTagsEditor .tags-input').value = 'local-tag-1, local-tag-2';
|
||||
document.getElementById('recipePromptInput').value = 'local prompt';
|
||||
document.getElementById('recipeNegativePromptInput').value = 'local negative';
|
||||
|
||||
@@ -899,7 +867,6 @@ describe('Interaction-level regression coverage', () => {
|
||||
await flushAsyncTasks();
|
||||
|
||||
expect(document.querySelector('#recipeTitleEditor .title-input').value).toBe('Local Title');
|
||||
expect(document.querySelector('#recipeTagsEditor .tags-input').value).toBe('local-tag-1, local-tag-2');
|
||||
expect(document.getElementById('recipePromptInput').value).toBe('local prompt');
|
||||
expect(document.getElementById('recipeNegativePromptInput').value).toBe('local negative');
|
||||
expect(recipeModal.currentRecipe.title).toBe('Hydrated Title');
|
||||
@@ -918,12 +885,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -1057,12 +1019,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -1170,8 +1127,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div id="recipeModal" class="modal">
|
||||
<div id="recipeModalTitle"></div>
|
||||
<div id="recipePreviewContainer"></div>
|
||||
<div id="recipeTagsCompact"></div>
|
||||
<div id="recipeTagsTooltip"><div id="recipeTagsTooltipContent"></div></div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
<div id="recipePrompt"></div>
|
||||
<textarea id="recipePromptInput"></textarea>
|
||||
<div id="recipeNegativePrompt"></div>
|
||||
@@ -1224,8 +1180,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div id="recipeModal" class="modal">
|
||||
<div id="recipeModalTitle"></div>
|
||||
<div id="recipePreviewContainer"></div>
|
||||
<div id="recipeTagsCompact"></div>
|
||||
<div id="recipeTagsTooltip"><div id="recipeTagsTooltipContent"></div></div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
<div id="recipePrompt"></div>
|
||||
<textarea id="recipePromptInput"></textarea>
|
||||
<div id="recipeNegativePrompt"></div>
|
||||
@@ -1300,12 +1255,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -1418,12 +1368,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -1541,12 +1486,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -1654,12 +1594,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -1776,12 +1711,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -1878,12 +1808,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
@@ -2007,12 +1932,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
<div class="modal-content">
|
||||
<header class="recipe-modal-header">
|
||||
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||
<div class="recipe-tags-container">
|
||||
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recipeTagsContainer"></div>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="recipe-top-section">
|
||||
|
||||
Reference in New Issue
Block a user