diff --git a/static/css/components/lora-modal/tag.css b/static/css/components/lora-modal/tag.css
index 3674025a..4ee49d43 100644
--- a/static/css/components/lora-modal/tag.css
+++ b/static/css/components/lora-modal/tag.css
@@ -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,
diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css
index 08981032..3445dc18 100644
--- a/static/css/components/recipe-modal.css
+++ b/static/css/components/recipe-modal.css
@@ -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;
}
diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js
index 02f5528d..e3598326 100644
--- a/static/js/components/RecipeModal.js
+++ b/static/js/components/RecipeModal.js
@@ -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 = `
-
-
-
-
- `;
-
- 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 = 'No tags
';
- }
-
- // 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 = 'No tags
';
- }
- }
-
- 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 = [
{
diff --git a/static/js/components/shared/ModelTags.js b/static/js/components/shared/ModelTags.js
index 922af248..b6226626 100644
--- a/static/js/components/shared/ModelTags.js
+++ b/static/js/components/shared/ModelTags.js
@@ -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;
- setActiveModelTypeKey(modelType);
- ensurePriorityTagSuggestions();
+ 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
- const tagForm = editContainer.querySelector('.metadata-add-form');
- const suggestionsDropdown = createSuggestionsDropdown(originalTags);
- tagForm.appendChild(suggestionsDropdown);
+ 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,59 +338,68 @@ async function saveTags() {
}
try {
- // Save tags metadata
- await getModelApiClient().saveModelMetadata(filePath, { tags: tags });
+ // 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";
- // 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 = '';
+ // 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 = scope;
+ if (compactTagsContainer) {
+ // Generate new compact tags HTML
+ const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact');
- // 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);
- });
+ 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);
+ }
+ }
- // 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);
+ });
}
}
- // 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();
}
- // Exit edit mode
- editBtn.click();
-
showToast('modelTags.messages.updated', {}, 'success');
} catch (error) {
console.error('Error saving tags:', 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();
diff --git a/static/js/components/shared/utils.js b/static/js/components/shared/utils.js
index a0226e05..e8b4a38b 100644
--- a/static/js/components/shared/utils.js
+++ b/static/js/components/shared/utils.js
@@ -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', () => {
diff --git a/templates/components/recipe_modal.html b/templates/components/recipe_modal.html
index 41327356..a9271bac 100644
--- a/templates/components/recipe_modal.html
+++ b/templates/components/recipe_modal.html
@@ -6,13 +6,8 @@
Recipe Details
-
-
+
+
diff --git a/tests/frontend/components/contextMenu.interactions.test.js b/tests/frontend/components/contextMenu.interactions.test.js
index 9fcc577e..fa413ba4 100644
--- a/tests/frontend/components/contextMenu.interactions.test.js
+++ b/tests/frontend/components/contextMenu.interactions.test.js
@@ -246,12 +246,7 @@ describe('Interaction-level regression coverage', () => {
@@ -375,12 +370,7 @@ describe('Interaction-level regression coverage', () => {
@@ -474,12 +464,7 @@ describe('Interaction-level regression coverage', () => {
@@ -588,12 +573,7 @@ describe('Interaction-level regression coverage', () => {
@@ -682,12 +662,7 @@ describe('Interaction-level regression coverage', () => {
@@ -790,12 +765,7 @@ describe('Interaction-level regression coverage', () => {
@@ -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', () => {
@@ -1057,12 +1019,7 @@ describe('Interaction-level regression coverage', () => {
@@ -1170,8 +1127,7 @@ describe('Interaction-level regression coverage', () => {
-
-
+
@@ -1224,8 +1180,7 @@ describe('Interaction-level regression coverage', () => {
-
-
+
@@ -1300,12 +1255,7 @@ describe('Interaction-level regression coverage', () => {
@@ -1418,12 +1368,7 @@ describe('Interaction-level regression coverage', () => {
@@ -1541,12 +1486,7 @@ describe('Interaction-level regression coverage', () => {
@@ -1654,12 +1594,7 @@ describe('Interaction-level regression coverage', () => {
@@ -1776,12 +1711,7 @@ describe('Interaction-level regression coverage', () => {
@@ -1878,12 +1808,7 @@ describe('Interaction-level regression coverage', () => {
@@ -2007,12 +1932,7 @@ describe('Interaction-level regression coverage', () => {