diff --git a/static/js/components/LoraModal.js b/static/js/components/LoraModal.js deleted file mode 100644 index 3f771e64..00000000 --- a/static/js/components/LoraModal.js +++ /dev/null @@ -1,1733 +0,0 @@ -import { showToast } from '../utils/uiHelpers.js'; -import { state } from '../state/index.js'; -import { modalManager } from '../managers/ModalManager.js'; -import { NSFW_LEVELS, BASE_MODELS } from '../utils/constants.js'; - -export function showLoraModal(lora) { - const escapedWords = lora.civitai?.trainedWords?.length ? - lora.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : []; - - const content = ` - - `; - - modalManager.showModal('loraModal', content); - setupEditableFields(); - setupShowcaseScroll(); - setupTabSwitching(); - setupTagTooltip(); - setupTriggerWordsEditMode(); - setupModelNameEditing(); - setupBaseModelEditing(); - setupFileNameEditing(); - - // If we have a model ID but no description, fetch it - if (lora.civitai?.modelId && !lora.modelDescription) { - loadModelDescription(lora.civitai.modelId, lora.file_path); - } -} - -// Function to render showcase content -function renderShowcaseContent(images) { - if (!images?.length) return '
No example images available
'; - - // Filter images based on SFW setting - const showOnlySFW = state.settings.show_only_sfw; - let filteredImages = images; - let hiddenCount = 0; - - if (showOnlySFW) { - filteredImages = images.filter(img => { - const nsfwLevel = img.nsfwLevel !== undefined ? img.nsfwLevel : 0; - const isSfw = nsfwLevel < NSFW_LEVELS.R; - if (!isSfw) hiddenCount++; - return isSfw; - }); - } - - // Show message if no images are available after filtering - if (filteredImages.length === 0) { - return ` -
-

All example images are filtered due to NSFW content settings

-

Your settings are currently set to show only safe-for-work content

-

You can change this in Settings

-
- `; - } - - // Show hidden content notification if applicable - const hiddenNotification = hiddenCount > 0 ? - `
- ${hiddenCount} ${hiddenCount === 1 ? 'image' : 'images'} hidden due to SFW-only setting -
` : ''; - - return ` -
- - Scroll or click to show ${filteredImages.length} examples -
- - `; -} - -// Helper function to generate video wrapper HTML -function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) { - return ` -
- ${shouldBlur ? ` - - ` : ''} - - ${shouldBlur ? ` -
-
-

${nsfwText}

- -
-
- ` : ''} - ${metadataPanel} -
- `; -} - -// Helper function to generate image wrapper HTML -function generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) { - return ` -
- ${shouldBlur ? ` - - ` : ''} - Preview - ${shouldBlur ? ` -
-
-

${nsfwText}

- -
-
- ` : ''} - ${metadataPanel} -
- `; -} - -// New function to handle tab switching -function setupTabSwitching() { - const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn'); - - tabButtons.forEach(button => { - button.addEventListener('click', () => { - // Remove active class from all tabs - document.querySelectorAll('.showcase-tabs .tab-btn').forEach(btn => - btn.classList.remove('active') - ); - document.querySelectorAll('.tab-content .tab-pane').forEach(tab => - tab.classList.remove('active') - ); - - // Add active class to clicked tab - button.classList.add('active'); - const tabId = `${button.dataset.tab}-tab`; - document.getElementById(tabId).classList.add('active'); - - // If switching to description tab, make sure content is properly sized - if (button.dataset.tab === 'description') { - const descriptionContent = document.querySelector('.model-description-content'); - if (descriptionContent) { - const hasContent = descriptionContent.innerHTML.trim() !== ''; - document.querySelector('.model-description-loading')?.classList.add('hidden'); - - // If no content, show a message - if (!hasContent) { - descriptionContent.innerHTML = '
No model description available
'; - descriptionContent.classList.remove('hidden'); - } - } - } - }); - }); -} - -// New function to load model description -async function loadModelDescription(modelId, filePath) { - try { - const descriptionContainer = document.querySelector('.model-description-content'); - const loadingElement = document.querySelector('.model-description-loading'); - - if (!descriptionContainer || !loadingElement) return; - - // Show loading indicator - loadingElement.classList.remove('hidden'); - descriptionContainer.classList.add('hidden'); - - // Try to get model description from API - const response = await fetch(`/api/lora-model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`); - - if (!response.ok) { - throw new Error(`Failed to fetch model description: ${response.statusText}`); - } - - const data = await response.json(); - - if (data.success && data.description) { - // Update the description content - descriptionContainer.innerHTML = data.description; - - // Process any links in the description to open in new tab - const links = descriptionContainer.querySelectorAll('a'); - links.forEach(link => { - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener noreferrer'); - }); - - // Show the description and hide loading indicator - descriptionContainer.classList.remove('hidden'); - loadingElement.classList.add('hidden'); - } else { - throw new Error(data.error || 'No description available'); - } - } catch (error) { - console.error('Error loading model description:', error); - const loadingElement = document.querySelector('.model-description-loading'); - if (loadingElement) { - loadingElement.innerHTML = `
Failed to load model description. ${error.message}
`; - } - - // Show empty state message in the description container - const descriptionContainer = document.querySelector('.model-description-content'); - if (descriptionContainer) { - descriptionContainer.innerHTML = '
No model description available
'; - descriptionContainer.classList.remove('hidden'); - } - } -} - -// 添加复制文件名的函数 -window.copyFileName = async function(fileName) { - try { - await navigator.clipboard.writeText(fileName); - showToast('File name copied', 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } -}; - -// Add function to save model name -window.saveModelName = async function(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 - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - if (loraCard) { - loraCard.dataset.model_name = newModelName; - const titleElement = loraCard.querySelector('.card-title'); - if (titleElement) { - titleElement.textContent = newModelName; - } - } - - showToast('Model name updated successfully', 'success'); - - // Reload the page to reflect the sorted order - setTimeout(() => { - window.location.reload(); - }, 1500); - } catch (error) { - showToast('Failed to update model name', 'error'); - } -}; - -function setupEditableFields() { - const editableFields = document.querySelectorAll('.editable-field [contenteditable]'); - - editableFields.forEach(field => { - field.addEventListener('focus', function() { - if (this.textContent === 'Add your notes here...') { - this.textContent = ''; - } - }); - - field.addEventListener('blur', function() { - if (this.textContent.trim() === '') { - if (this.classList.contains('notes-content')) { - this.textContent = 'Add your notes here...'; - } - } - }); - }); - - const presetSelector = document.getElementById('preset-selector'); - const presetValue = document.getElementById('preset-value'); - const addPresetBtn = document.querySelector('.add-preset-btn'); - const presetTags = document.querySelector('.preset-tags'); - - presetSelector.addEventListener('change', function() { - const selected = this.value; - if (selected) { - presetValue.style.display = 'inline-block'; - presetValue.min = selected.includes('strength') ? -10 : 0; - presetValue.max = selected.includes('strength') ? 10 : 10; - presetValue.step = 0.5; - if (selected === 'clip_skip') { - presetValue.type = 'number'; - presetValue.step = 1; - } - // Add auto-focus - setTimeout(() => presetValue.focus(), 0); - } else { - presetValue.style.display = 'none'; - } - }); - - addPresetBtn.addEventListener('click', async function() { - const key = presetSelector.value; - const value = presetValue.value; - - if (!key || !value) return; - - const filePath = document.querySelector('#loraModal .modal-content') - .querySelector('.file-path').textContent + - document.querySelector('#loraModal .modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - const currentPresets = parsePresets(loraCard.dataset.usage_tips); - - currentPresets[key] = parseFloat(value); - const newPresetsJson = JSON.stringify(currentPresets); - - await saveModelMetadata(filePath, { - usage_tips: newPresetsJson - }); - - loraCard.dataset.usage_tips = newPresetsJson; - presetTags.innerHTML = renderPresetTags(currentPresets); - - presetSelector.value = ''; - presetValue.value = ''; - presetValue.style.display = 'none'; - }); - - // Add keydown event listeners for notes - const notesContent = document.querySelector('.notes-content'); - if (notesContent) { - notesContent.addEventListener('keydown', async function(e) { - if (e.key === 'Enter') { - if (e.shiftKey) { - // Allow shift+enter for new line - return; - } - e.preventDefault(); - const filePath = document.querySelector('#loraModal .modal-content') - .querySelector('.file-path').textContent + - document.querySelector('#loraModal .modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - await saveNotes(filePath); - } - }); - } - - // Add keydown event for preset value - presetValue.addEventListener('keydown', function(e) { - if (e.key === 'Enter') { - e.preventDefault(); - addPresetBtn.click(); - } - }); -} - -window.saveNotes = async function(filePath) { - const content = document.querySelector('.notes-content').textContent; - try { - await saveModelMetadata(filePath, { notes: content }); - - // Update the corresponding lora card's dataset - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - if (loraCard) { - loraCard.dataset.notes = content; - } - - showToast('Notes saved successfully', 'success'); - } catch (error) { - showToast('Failed to save notes', 'error'); - } -}; - -async function saveModelMetadata(filePath, data) { - const response = await fetch('/api/loras/save-metadata', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath, - ...data - }) - }); - - if (!response.ok) { - throw new Error('Failed to save metadata'); - } -} - -function renderTriggerWords(words, filePath) { - if (!words.length) return ` -
-
- - -
-
- No trigger word needed - -
- - -
- `; - - return ` -
-
- - -
-
-
- ${words.map(word => ` -
- ${word} - - - - -
- `).join('')} -
-
- - -
- `; -} - -export function toggleShowcase(element) { - const carousel = element.nextElementSibling; - const isCollapsed = carousel.classList.contains('collapsed'); - const indicator = element.querySelector('span'); - const icon = element.querySelector('i'); - - carousel.classList.toggle('collapsed'); - - if (isCollapsed) { - const count = carousel.querySelectorAll('.media-wrapper').length; - indicator.textContent = `Scroll or click to hide examples`; - icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); - initLazyLoading(carousel); - - // Initialize NSFW content blur toggle handlers - initNsfwBlurHandlers(carousel); - - // Initialize metadata panel interaction handlers - initMetadataPanelHandlers(carousel); - } else { - const count = carousel.querySelectorAll('.media-wrapper').length; - indicator.textContent = `Scroll or click to show ${count} examples`; - icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); - - // Make sure any open metadata panels get closed - const carouselContainer = carousel.querySelector('.carousel-container'); - if (carouselContainer) { - carouselContainer.style.height = '0'; - setTimeout(() => { - carouselContainer.style.height = ''; - }, 300); - } - } -} - -// Function to initialize metadata panel interactions -function initMetadataPanelHandlers(container) { - // Find all media wrappers - const mediaWrappers = container.querySelectorAll('.media-wrapper'); - - mediaWrappers.forEach(wrapper => { - // Get the metadata panel - const metadataPanel = wrapper.querySelector('.image-metadata-panel'); - if (!metadataPanel) return; - - // Prevent events from the metadata panel from bubbling - metadataPanel.addEventListener('click', (e) => { - e.stopPropagation(); - }); - - // Handle copy prompt button clicks - const copyBtns = metadataPanel.querySelectorAll('.copy-prompt-btn'); - copyBtns.forEach(copyBtn => { - const promptIndex = copyBtn.dataset.promptIndex; - const promptElement = wrapper.querySelector(`#prompt-${promptIndex}`); - - copyBtn.addEventListener('click', async (e) => { - e.stopPropagation(); // Prevent bubbling - - if (!promptElement) return; - - try { - await navigator.clipboard.writeText(promptElement.textContent); - showToast('Prompt copied to clipboard', 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } - }); - }); - - // Prevent scrolling in the metadata panel from scrolling the whole modal - metadataPanel.addEventListener('wheel', (e) => { - const isAtTop = metadataPanel.scrollTop === 0; - const isAtBottom = metadataPanel.scrollHeight - metadataPanel.scrollTop === metadataPanel.clientHeight; - - // Only prevent default if scrolling would cause the panel to scroll - if ((e.deltaY < 0 && !isAtTop) || (e.deltaY > 0 && !isAtBottom)) { - e.stopPropagation(); - } - }, { passive: true }); - }); -} - -// New function to initialize blur toggle handlers for showcase images/videos -function initNsfwBlurHandlers(container) { - // Handle toggle blur buttons - const toggleButtons = container.querySelectorAll('.toggle-blur-btn'); - toggleButtons.forEach(btn => { - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const wrapper = btn.closest('.media-wrapper'); - const media = wrapper.querySelector('img, video'); - const isBlurred = media.classList.toggle('blurred'); - const icon = btn.querySelector('i'); - - // Update the icon based on blur state - if (isBlurred) { - icon.className = 'fas fa-eye'; - } else { - icon.className = 'fas fa-eye-slash'; - } - - // Toggle the overlay visibility - const overlay = wrapper.querySelector('.nsfw-overlay'); - if (overlay) { - overlay.style.display = isBlurred ? 'flex' : 'none'; - } - }); - }); - - // Handle "Show" buttons in overlays - const showButtons = container.querySelectorAll('.show-content-btn'); - showButtons.forEach(btn => { - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const wrapper = btn.closest('.media-wrapper'); - const media = wrapper.querySelector('img, video'); - media.classList.remove('blurred'); - - // Update the toggle button icon - const toggleBtn = wrapper.querySelector('.toggle-blur-btn'); - if (toggleBtn) { - toggleBtn.querySelector('i').className = 'fas fa-eye-slash'; - } - - // Hide the overlay - const overlay = wrapper.querySelector('.nsfw-overlay'); - if (overlay) { - overlay.style.display = 'none'; - } - }); - }); -} - -// Add lazy loading initialization -function initLazyLoading(container) { - const lazyElements = container.querySelectorAll('.lazy'); - - const lazyLoad = (element) => { - if (element.tagName.toLowerCase() === 'video') { - element.src = element.dataset.src; - element.querySelector('source').src = element.dataset.src; - element.load(); - } else { - element.src = element.dataset.src; - } - element.classList.remove('lazy'); - }; - - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - lazyLoad(entry.target); - observer.unobserve(entry.target); - } - }); - }); - - lazyElements.forEach(element => observer.observe(element)); -} - -export function setupShowcaseScroll() { - // Add event listener to document for wheel events - document.addEventListener('wheel', (event) => { - // Find the active modal content - const modalContent = document.querySelector('#loraModal .modal-content'); - if (!modalContent) return; - - const showcase = modalContent.querySelector('.showcase-section'); - if (!showcase) return; - - const carousel = showcase.querySelector('.carousel'); - const scrollIndicator = showcase.querySelector('.scroll-indicator'); - - if (carousel?.classList.contains('collapsed') && event.deltaY > 0) { - const isNearBottom = modalContent.scrollHeight - modalContent.scrollTop - modalContent.clientHeight < 100; - - if (isNearBottom) { - toggleShowcase(scrollIndicator); - event.preventDefault(); - } - } - }, { passive: false }); - - // Use MutationObserver instead of deprecated DOMNodeInserted - const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - if (mutation.type === 'childList' && mutation.addedNodes.length) { - // Check if loraModal content was added - const loraModal = document.getElementById('loraModal'); - if (loraModal && loraModal.querySelector('.modal-content')) { - setupBackToTopButton(loraModal.querySelector('.modal-content')); - } - } - } - }); - - // Start observing the document body for changes - observer.observe(document.body, { childList: true, subtree: true }); - - // Also try to set up the button immediately in case the modal is already open - const modalContent = document.querySelector('#loraModal .modal-content'); - if (modalContent) { - setupBackToTopButton(modalContent); - } -} - -// New helper function to set up the back to top button -function setupBackToTopButton(modalContent) { - // Remove any existing scroll listeners to avoid duplicates - modalContent.onscroll = null; - - // Add new scroll listener - modalContent.addEventListener('scroll', () => { - const backToTopBtn = modalContent.querySelector('.back-to-top'); - if (backToTopBtn) { - if (modalContent.scrollTop > 300) { - backToTopBtn.classList.add('visible'); - } else { - backToTopBtn.classList.remove('visible'); - } - } - }); - - // Trigger a scroll event to check initial position - modalContent.dispatchEvent(new Event('scroll')); -} - -export function scrollToTop(button) { - const modalContent = button.closest('.modal-content'); - if (modalContent) { - modalContent.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } -} - -function parsePresets(usageTips) { - if (!usageTips) return {}; - try { - return JSON.parse(usageTips); - } catch { - return {}; - } -} - -function renderPresetTags(presets) { - return Object.entries(presets).map(([key, value]) => ` -
- ${formatPresetKey(key)}: ${value} - -
- `).join(''); -} - -function formatPresetKey(key) { - return key.split('_').map(word => - word.charAt(0).toUpperCase() + word.slice(1) - ).join(' '); -} - -window.removePreset = async function(key) { - const filePath = document.querySelector('#loraModal .modal-content') - .querySelector('.file-path').textContent + - document.querySelector('#loraModal .modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - const currentPresets = parsePresets(loraCard.dataset.usage_tips); - - delete currentPresets[key]; - const newPresetsJson = JSON.stringify(currentPresets); - - await saveModelMetadata(filePath, { - usage_tips: newPresetsJson - }); - - loraCard.dataset.usage_tips = newPresetsJson; - document.querySelector('.preset-tags').innerHTML = renderPresetTags(currentPresets); -}; - -// 添加文件大小格式化函数 -function formatFileSize(bytes) { - if (!bytes) return 'N/A'; - const units = ['B', 'KB', 'MB', 'GB']; - let size = bytes; - let unitIndex = 0; - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - - return `${size.toFixed(1)} ${units[unitIndex]}`; -} - -// New function to render compact tags with tooltip -function renderCompactTags(tags) { - if (!tags || tags.length === 0) return ''; - - // Display up to 5 tags, with a tooltip indicator if there are more - const visibleTags = tags.slice(0, 5); - const remainingCount = Math.max(0, tags.length - 5); - - return ` -
-
- ${visibleTags.map(tag => `${tag}`).join('')} - ${remainingCount > 0 ? - `+${remainingCount}` : - ''} -
- ${tags.length > 0 ? - `
-
- ${tags.map(tag => `${tag}`).join('')} -
-
` : - ''} -
- `; -} - -// Setup tooltip functionality -function setupTagTooltip() { - const tagsContainer = document.querySelector('.model-tags-container'); - const tooltip = document.querySelector('.model-tags-tooltip'); - - if (tagsContainer && tooltip) { - tagsContainer.addEventListener('mouseenter', () => { - tooltip.classList.add('visible'); - }); - - tagsContainer.addEventListener('mouseleave', () => { - tooltip.classList.remove('visible'); - }); - } -} - -// Set up trigger words edit mode -function setupTriggerWordsEditMode() { - const editBtn = document.querySelector('.edit-trigger-words-btn'); - if (!editBtn) return; - - editBtn.addEventListener('click', function() { - const triggerWordsSection = this.closest('.trigger-words'); - const isEditMode = triggerWordsSection.classList.toggle('edit-mode'); - - // Toggle edit mode UI elements - const triggerWordTags = triggerWordsSection.querySelectorAll('.trigger-word-tag'); - const editControls = triggerWordsSection.querySelector('.trigger-words-edit-controls'); - const noTriggerWords = triggerWordsSection.querySelector('.no-trigger-words'); - const tagsContainer = triggerWordsSection.querySelector('.trigger-words-tags'); - - if (isEditMode) { - this.innerHTML = ''; // Change to cancel icon - this.title = "Cancel editing"; - editControls.style.display = 'flex'; - - // If we have no trigger words yet, hide the "No trigger word needed" text - // and show the empty tags container - if (noTriggerWords) { - noTriggerWords.style.display = 'none'; - if (tagsContainer) tagsContainer.style.display = 'flex'; - } - - // Disable click-to-copy and show delete buttons - triggerWordTags.forEach(tag => { - tag.onclick = null; - tag.querySelector('.trigger-word-copy').style.display = 'none'; - tag.querySelector('.delete-trigger-word-btn').style.display = 'block'; - }); - } else { - this.innerHTML = ''; // Change back to edit icon - this.title = "Edit trigger words"; - editControls.style.display = 'none'; - - // If we have no trigger words, show the "No trigger word needed" text - // and hide the empty tags container - const currentTags = triggerWordsSection.querySelectorAll('.trigger-word-tag'); - if (noTriggerWords && currentTags.length === 0) { - noTriggerWords.style.display = ''; - if (tagsContainer) tagsContainer.style.display = 'none'; - } - - // Restore original state - triggerWordTags.forEach(tag => { - const word = tag.dataset.word; - tag.onclick = () => copyTriggerWord(word); - tag.querySelector('.trigger-word-copy').style.display = 'flex'; - tag.querySelector('.delete-trigger-word-btn').style.display = 'none'; - }); - - // Hide add form if open - triggerWordsSection.querySelector('.add-trigger-word-form').style.display = 'none'; - } - }); - - // Set up add trigger word button - const addBtn = document.querySelector('.add-trigger-word-btn'); - if (addBtn) { - addBtn.addEventListener('click', function() { - const triggerWordsSection = this.closest('.trigger-words'); - const addForm = triggerWordsSection.querySelector('.add-trigger-word-form'); - addForm.style.display = 'flex'; - addForm.querySelector('input').focus(); - }); - } - - // Set up confirm and cancel add buttons - const confirmAddBtn = document.querySelector('.confirm-add-trigger-word-btn'); - const cancelAddBtn = document.querySelector('.cancel-add-trigger-word-btn'); - const triggerWordInput = document.querySelector('.new-trigger-word-input'); - - if (confirmAddBtn && triggerWordInput) { - confirmAddBtn.addEventListener('click', function() { - addNewTriggerWord(triggerWordInput.value); - }); - - // Add keydown event to input - triggerWordInput.addEventListener('keydown', function(e) { - if (e.key === 'Enter') { - e.preventDefault(); - addNewTriggerWord(this.value); - } - }); - } - - if (cancelAddBtn) { - cancelAddBtn.addEventListener('click', function() { - const addForm = this.closest('.add-trigger-word-form'); - addForm.style.display = 'none'; - addForm.querySelector('input').value = ''; - }); - } - - // Set up save button - const saveBtn = document.querySelector('.save-trigger-words-btn'); - if (saveBtn) { - saveBtn.addEventListener('click', saveTriggerWords); - } - - // Set up delete buttons - document.querySelectorAll('.delete-trigger-word-btn').forEach(btn => { - btn.addEventListener('click', function(e) { - e.stopPropagation(); - const tag = this.closest('.trigger-word-tag'); - tag.remove(); - }); - }); -} - -// Function to add a new trigger word -function addNewTriggerWord(word) { - word = word.trim(); - if (!word) return; - - const triggerWordsSection = document.querySelector('.trigger-words'); - let tagsContainer = document.querySelector('.trigger-words-tags'); - - // Ensure tags container exists and is visible - if (tagsContainer) { - tagsContainer.style.display = 'flex'; - } else { - // Create tags container if it doesn't exist - const contentDiv = triggerWordsSection.querySelector('.trigger-words-content'); - if (contentDiv) { - tagsContainer = document.createElement('div'); - tagsContainer.className = 'trigger-words-tags'; - contentDiv.appendChild(tagsContainer); - } - } - - if (!tagsContainer) return; - - // Hide "no trigger words" message if it exists - const noTriggerWordsMsg = triggerWordsSection.querySelector('.no-trigger-words'); - if (noTriggerWordsMsg) { - noTriggerWordsMsg.style.display = 'none'; - } - - // Validation: Check length - if (word.split(/\s+/).length > 30) { - showToast('Trigger word should not exceed 30 words', 'error'); - return; - } - - // Validation: Check total number - const currentTags = tagsContainer.querySelectorAll('.trigger-word-tag'); - if (currentTags.length >= 10) { - showToast('Maximum 10 trigger words allowed', 'error'); - return; - } - - // Validation: Check for duplicates - const existingWords = Array.from(currentTags).map(tag => tag.dataset.word); - if (existingWords.includes(word)) { - showToast('This trigger word already exists', 'error'); - return; - } - - // Create new tag - const newTag = document.createElement('div'); - newTag.className = 'trigger-word-tag'; - newTag.dataset.word = word; - newTag.innerHTML = ` - ${word} - - - `; - - // Add event listener to delete button - const deleteBtn = newTag.querySelector('.delete-trigger-word-btn'); - deleteBtn.addEventListener('click', function() { - newTag.remove(); - }); - - tagsContainer.appendChild(newTag); - - // Clear and hide the input form - const triggerWordInput = document.querySelector('.new-trigger-word-input'); - triggerWordInput.value = ''; - document.querySelector('.add-trigger-word-form').style.display = 'none'; -} - -// Function to save updated trigger words -async function saveTriggerWords() { - const filePath = document.querySelector('.edit-trigger-words-btn').dataset.filePath; - const triggerWordTags = document.querySelectorAll('.trigger-word-tag'); - const words = Array.from(triggerWordTags).map(tag => tag.dataset.word); - - try { - // Special format for updating nested civitai.trainedWords - await saveModelMetadata(filePath, { - civitai: { trainedWords: words } - }); - - // Update UI - const editBtn = document.querySelector('.edit-trigger-words-btn'); - editBtn.click(); // Exit edit mode - - // Update the LoRA card's dataset - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - if (loraCard) { - try { - // Create a proper structure for civitai data - let civitaiData = {}; - - // Parse existing data if available - if (loraCard.dataset.meta) { - civitaiData = JSON.parse(loraCard.dataset.meta); - } - - // Update trainedWords property - civitaiData.trainedWords = words; - - // Update the meta dataset attribute with the full civitai data - loraCard.dataset.meta = JSON.stringify(civitaiData); - - // For debugging, log the updated data to verify it's correct - console.log("Updated civitai data:", civitaiData); - } catch (e) { - console.error('Error updating civitai data:', e); - } - } - - // If we saved an empty array and there's a no-trigger-words element, show it - const noTriggerWords = document.querySelector('.no-trigger-words'); - const tagsContainer = document.querySelector('.trigger-words-tags'); - if (words.length === 0 && noTriggerWords) { - noTriggerWords.style.display = ''; - if (tagsContainer) tagsContainer.style.display = 'none'; - } - - showToast('Trigger words updated successfully', 'success'); - } catch (error) { - console.error('Error saving trigger words:', error); - showToast('Failed to update trigger words', 'error'); - } -} - -// Add copy trigger word function -window.copyTriggerWord = async function(word) { - try { - await navigator.clipboard.writeText(word); - showToast('Trigger word copied', 'success'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } -}; - -// New function to handle model name editing -function setupModelNameEditing() { - const modelNameContent = document.querySelector('.model-name-content'); - const editBtn = document.querySelector('.edit-model-name-btn'); - - if (!modelNameContent || !editBtn) return; - - // Show edit button on hover - const modelNameHeader = document.querySelector('.model-name-header'); - modelNameHeader.addEventListener('mouseenter', () => { - editBtn.classList.add('visible'); - }); - - modelNameHeader.addEventListener('mouseleave', () => { - if (!modelNameContent.getAttribute('data-editing')) { - editBtn.classList.remove('visible'); - } - }); - - // Handle edit button click - editBtn.addEventListener('click', () => { - modelNameContent.setAttribute('data-editing', 'true'); - modelNameContent.focus(); - - // Place cursor at the end - const range = document.createRange(); - const sel = window.getSelection(); - if (modelNameContent.childNodes.length > 0) { - range.setStart(modelNameContent.childNodes[0], modelNameContent.textContent.length); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - } - - editBtn.classList.add('visible'); - }); - - // Handle focus out - modelNameContent.addEventListener('blur', function() { - this.removeAttribute('data-editing'); - editBtn.classList.remove('visible'); - - if (this.textContent.trim() === '') { - // Restore original model name if empty - const filePath = document.querySelector('#loraModal .modal-content') - .querySelector('.file-path').textContent + - document.querySelector('#loraModal .modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - if (loraCard) { - this.textContent = loraCard.dataset.model_name; - } - } - }); - - // Handle enter key - modelNameContent.addEventListener('keydown', function(e) { - if (e.key === 'Enter') { - e.preventDefault(); - const filePath = document.querySelector('#loraModal .modal-content') - .querySelector('.file-path').textContent + - document.querySelector('#loraModal .modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - saveModelName(filePath); - this.blur(); - } - }); - - // Limit model name length - modelNameContent.addEventListener('input', function() { - // Limit model name length - if (this.textContent.length > 100) { - this.textContent = this.textContent.substring(0, 100); - // Place cursor at the end - const range = document.createRange(); - const sel = window.getSelection(); - range.setStart(this.childNodes[0], 100); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - - showToast('Model name is limited to 100 characters', 'warning'); - } - }); -} - -// Add save model base model function -window.saveBaseModel = async function(filePath, originalValue) { - const baseModelElement = document.querySelector('.base-model-content'); - const newBaseModel = baseModelElement.textContent.trim(); - - // Only save if the value has actually changed - if (newBaseModel === originalValue) { - return; // No change, no need to save - } - - try { - await saveModelMetadata(filePath, { base_model: newBaseModel }); - - // Update the corresponding lora card's dataset - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - if (loraCard) { - loraCard.dataset.base_model = newBaseModel; - } - - showToast('Base model updated successfully', 'success'); - } catch (error) { - showToast('Failed to update base model', 'error'); - } -}; - -// New function to handle base model editing -function setupBaseModelEditing() { - const baseModelContent = document.querySelector('.base-model-content'); - const editBtn = document.querySelector('.edit-base-model-btn'); - - if (!baseModelContent || !editBtn) return; - - // Show edit button on hover - const baseModelDisplay = document.querySelector('.base-model-display'); - baseModelDisplay.addEventListener('mouseenter', () => { - editBtn.classList.add('visible'); - }); - - baseModelDisplay.addEventListener('mouseleave', () => { - if (!baseModelDisplay.classList.contains('editing')) { - editBtn.classList.remove('visible'); - } - }); - - // Handle edit button click - editBtn.addEventListener('click', () => { - baseModelDisplay.classList.add('editing'); - - // Store the original value to check for changes later - const originalValue = baseModelContent.textContent.trim(); - - // Create dropdown selector to replace the base model content - const currentValue = originalValue; - const dropdown = document.createElement('select'); - dropdown.className = 'base-model-selector'; - - // Flag to track if a change was made - let valueChanged = false; - - // Add options from BASE_MODELS constants - const baseModelCategories = { - 'Stable Diffusion 1.x': [BASE_MODELS.SD_1_4, BASE_MODELS.SD_1_5, BASE_MODELS.SD_1_5_LCM, BASE_MODELS.SD_1_5_HYPER], - 'Stable Diffusion 2.x': [BASE_MODELS.SD_2_0, BASE_MODELS.SD_2_1], - 'Stable Diffusion 3.x': [BASE_MODELS.SD_3, BASE_MODELS.SD_3_5, BASE_MODELS.SD_3_5_MEDIUM, BASE_MODELS.SD_3_5_LARGE, BASE_MODELS.SD_3_5_LARGE_TURBO], - 'SDXL': [BASE_MODELS.SDXL, BASE_MODELS.SDXL_LIGHTNING, BASE_MODELS.SDXL_HYPER], - 'Video Models': [BASE_MODELS.SVD, BASE_MODELS.WAN_VIDEO, BASE_MODELS.HUNYUAN_VIDEO], - 'Other Models': [ - BASE_MODELS.FLUX_1_D, BASE_MODELS.FLUX_1_S, BASE_MODELS.AURAFLOW, - BASE_MODELS.PIXART_A, BASE_MODELS.PIXART_E, BASE_MODELS.HUNYUAN_1, - BASE_MODELS.LUMINA, BASE_MODELS.KOLORS, BASE_MODELS.NOOBAI, - BASE_MODELS.ILLUSTRIOUS, BASE_MODELS.PONY, BASE_MODELS.UNKNOWN - ] - }; - - // Create option groups for better organization - Object.entries(baseModelCategories).forEach(([category, models]) => { - const group = document.createElement('optgroup'); - group.label = category; - - models.forEach(model => { - const option = document.createElement('option'); - option.value = model; - option.textContent = model; - option.selected = model === currentValue; - group.appendChild(option); - }); - - dropdown.appendChild(group); - }); - - // Replace content with dropdown - baseModelContent.style.display = 'none'; - baseModelDisplay.insertBefore(dropdown, editBtn); - - // Hide edit button during editing - editBtn.style.display = 'none'; - - // Focus the dropdown - dropdown.focus(); - - // Handle dropdown change - dropdown.addEventListener('change', function() { - const selectedModel = this.value; - baseModelContent.textContent = selectedModel; - - // Mark that a change was made if the value differs from original - if (selectedModel !== originalValue) { - valueChanged = true; - } else { - valueChanged = false; - } - }); - - // Function to save changes and exit edit mode - const saveAndExit = function() { - // Check if dropdown still exists and remove it - if (dropdown && dropdown.parentNode === baseModelDisplay) { - baseModelDisplay.removeChild(dropdown); - } - - // Show the content and edit button - baseModelContent.style.display = ''; - editBtn.style.display = ''; - - // Remove editing class - baseModelDisplay.classList.remove('editing'); - - // Only save if the value has actually changed - if (valueChanged || baseModelContent.textContent.trim() !== originalValue) { - // Get file path for saving - const filePath = document.querySelector('#loraModal .modal-content') - .querySelector('.file-path').textContent + - document.querySelector('#loraModal .modal-content') - .querySelector('#file-name').textContent + '.safetensors'; - - // Save the changes, passing the original value for comparison - saveBaseModel(filePath, originalValue); - } - - // Remove this event listener - document.removeEventListener('click', outsideClickHandler); - }; - - // Handle outside clicks to save and exit - const outsideClickHandler = function(e) { - // If click is outside the dropdown and base model display - if (!baseModelDisplay.contains(e.target)) { - saveAndExit(); - } - }; - - // Add delayed event listener for outside clicks - setTimeout(() => { - document.addEventListener('click', outsideClickHandler); - }, 0); - - // Also handle dropdown blur event - dropdown.addEventListener('blur', function(e) { - // Only save if the related target is not the edit button or inside the baseModelDisplay - if (!baseModelDisplay.contains(e.relatedTarget)) { - saveAndExit(); - } - }); - }); -} - -// New function to handle file name editing -function setupFileNameEditing() { - const fileNameContent = document.querySelector('.file-name-content'); - const editBtn = document.querySelector('.edit-file-name-btn'); - - if (!fileNameContent || !editBtn) return; - - // Show edit button on hover - const fileNameWrapper = document.querySelector('.file-name-wrapper'); - fileNameWrapper.addEventListener('mouseenter', () => { - editBtn.classList.add('visible'); - }); - - fileNameWrapper.addEventListener('mouseleave', () => { - if (!fileNameWrapper.classList.contains('editing')) { - editBtn.classList.remove('visible'); - } - }); - - // Handle edit button click - editBtn.addEventListener('click', () => { - fileNameWrapper.classList.add('editing'); - fileNameContent.setAttribute('contenteditable', 'true'); - fileNameContent.focus(); - - // Store original value for comparison later - fileNameContent.dataset.originalValue = fileNameContent.textContent.trim(); - - // Place cursor at the end - const range = document.createRange(); - const sel = window.getSelection(); - range.selectNodeContents(fileNameContent); - range.collapse(false); - sel.removeAllRanges(); - sel.addRange(range); - - editBtn.classList.add('visible'); - }); - - // Handle keyboard events in edit mode - fileNameContent.addEventListener('keydown', function(e) { - if (!this.getAttribute('contenteditable')) return; - - if (e.key === 'Enter') { - e.preventDefault(); - this.blur(); // Trigger save on Enter - } else if (e.key === 'Escape') { - e.preventDefault(); - // Restore original value - this.textContent = this.dataset.originalValue; - exitEditMode(); - } - }); - - // Handle input validation - fileNameContent.addEventListener('input', function() { - if (!this.getAttribute('contenteditable')) return; - - // Replace invalid characters for filenames - const invalidChars = /[\\/:*?"<>|]/g; - if (invalidChars.test(this.textContent)) { - const cursorPos = window.getSelection().getRangeAt(0).startOffset; - this.textContent = this.textContent.replace(invalidChars, ''); - - // Restore cursor position - const range = document.createRange(); - const sel = window.getSelection(); - const newPos = Math.min(cursorPos, this.textContent.length); - - if (this.firstChild) { - range.setStart(this.firstChild, newPos); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - } - - showToast('Invalid characters removed from filename', 'warning'); - } - }); - - // Handle focus out - save changes - fileNameContent.addEventListener('blur', async function() { - if (!this.getAttribute('contenteditable')) return; - - const newFileName = this.textContent.trim(); - const originalValue = this.dataset.originalValue; - - // Basic validation - if (!newFileName) { - // Restore original value if empty - this.textContent = originalValue; - showToast('File name cannot be empty', 'error'); - exitEditMode(); - return; - } - - if (newFileName === originalValue) { - // No changes, just exit edit mode - exitEditMode(); - return; - } - - try { - // Get the full file path - const filePath = document.querySelector('#loraModal .modal-content') - .querySelector('.file-path').textContent + originalValue + '.safetensors'; - - // Call API to rename the file - const response = await fetch('/api/rename_lora', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath, - new_file_name: newFileName - }) - }); - - const result = await response.json(); - - if (result.success) { - showToast('File name updated successfully', 'success'); - - // Update card in the gallery - const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - if (loraCard) { - // Update the card's filepath attribute to the new path - loraCard.dataset.filepath = result.new_file_path; - loraCard.dataset.file_name = newFileName; - - // Update the filename display in the card - const cardFileName = loraCard.querySelector('.card-filename'); - if (cardFileName) { - cardFileName.textContent = newFileName; - } - } - - // Handle the case where we need to reload the page - if (result.reload_required) { - showToast('Reloading page to apply changes...', 'info'); - setTimeout(() => { - window.location.reload(); - }, 1500); - } - } else { - // Show error and restore original filename - showToast(result.error || 'Failed to update file name', 'error'); - this.textContent = originalValue; - } - } catch (error) { - console.error('Error saving filename:', error); - showToast('Failed to update file name', 'error'); - this.textContent = originalValue; - } finally { - exitEditMode(); - } - }); - - function exitEditMode() { - fileNameContent.removeAttribute('contenteditable'); - fileNameWrapper.classList.remove('editing'); - editBtn.classList.remove('visible'); - } -} diff --git a/static/js/components/loraModal/ModelMetadata.js b/static/js/components/loraModal/ModelMetadata.js index b5eb72ac..eb0a3d18 100644 --- a/static/js/components/loraModal/ModelMetadata.js +++ b/static/js/components/loraModal/ModelMetadata.js @@ -452,12 +452,8 @@ export function setupFileNameEditing(filePath) { // Get the new file path and update the card const newFilePath = filePath.replace(originalValue, newFileName); - updateLoraCard(filePath, {}, newFilePath); - - // Reload the page after a short delay to reflect changes - setTimeout(() => { - window.location.reload(); - }, 1500); + // Pass the new file_name in the updates object for proper card update + updateLoraCard(filePath, { file_name: newFileName }, newFilePath); } else { throw new Error(result.error || 'Unknown error'); } diff --git a/static/js/utils/cardUpdater.js b/static/js/utils/cardUpdater.js index 7f5b69f6..ce099f98 100644 --- a/static/js/utils/cardUpdater.js +++ b/static/js/utils/cardUpdater.js @@ -86,6 +86,11 @@ export function updateLoraCard(filePath, updates, newFilePath) { if (modelNameElement) modelNameElement.textContent = value; break; + case 'file_name': + // Update the file_name in the dataset + loraCard.dataset.file_name = value; + break; + case 'base_model': // Update the base model label in the card header if it exists const baseModelLabel = loraCard.querySelector('.base-model-label');