diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index cde1284a..ddfdf8b2 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -384,7 +384,7 @@ class ApiRoutes: # 准备要处理的 loras to_process = [ lora for lora in cache.raw_data - if lora.get('sha256') and not lora.get('civitai') and lora.get('from_civitai') + if lora.get('sha256') and (not lora.get('civitai') or 'id' not in lora.get('civitai')) and lora.get('from_civitai') # TODO: for lora not from CivitAI but added traineWords ] total_to_process = len(to_process) @@ -617,8 +617,15 @@ class ApiRoutes: else: metadata = {} - # Update metadata with new values - metadata.update(metadata_updates) + # Handle nested updates (for civitai.trainedWords) + for key, value in metadata_updates.items(): + if isinstance(value, dict) and key in metadata and isinstance(metadata[key], dict): + # Deep update for nested dictionaries + for nested_key, nested_value in value.items(): + metadata[key][nested_key] = nested_value + else: + # Regular update for top-level keys + metadata[key] = value # Save updated metadata with open(metadata_path, 'w', encoding='utf-8') as f: diff --git a/py/utils/file_utils.py b/py/utils/file_utils.py index f9758e12..2ae05a60 100644 --- a/py/utils/file_utils.py +++ b/py/utils/file_utils.py @@ -121,6 +121,15 @@ async def load_metadata(file_path: str) -> Optional[LoraMetadata]: elif preview_url != normalize_path(preview_url): data['preview_url'] = normalize_path(preview_url) needs_update = True + + # Ensure all fields are present, due to updates adding new fields + if 'tags' not in data: + data['tags'] = [] + needs_update = True + + if 'modelDescription' not in data: + data['modelDescription'] = "" + needs_update = True if needs_update: with open(metadata_path, 'w', encoding='utf-8') as f: diff --git a/static/css/components/lora-modal.css b/static/css/components/lora-modal.css index 7ecd93ad..df2bd828 100644 --- a/static/css/components/lora-modal.css +++ b/static/css/components/lora-modal.css @@ -163,12 +163,57 @@ border: 1px solid var(--lora-border); } +/* New header style for trigger words */ +.trigger-words-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.edit-trigger-words-btn { + background: transparent; + border: none; + color: var(--text-color); + opacity: 0.5; + cursor: pointer; + padding: 2px 5px; + border-radius: var(--border-radius-xs); + transition: all 0.2s ease; +} + +.edit-trigger-words-btn:hover { + opacity: 0.8; + background: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .edit-trigger-words-btn:hover { + background: rgba(255, 255, 255, 0.05); +} + +/* Edit mode active state */ +.trigger-words.edit-mode .edit-trigger-words-btn { + opacity: 0.8; + color: var(--lora-accent); +} + +.trigger-words-content { + margin-bottom: var(--space-1); +} + .trigger-words-tags { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; - margin-top: var(--space-1); +} + +/* No trigger words message */ +.no-trigger-words { + color: var(--text-color); + opacity: 0.7; + font-style: italic; + font-size: 0.9em; } /* Update Trigger Words styles */ @@ -182,6 +227,7 @@ cursor: pointer; transition: all 0.2s ease; gap: 6px; + position: relative; } /* Update trigger word content color to use theme accent */ @@ -207,6 +253,123 @@ transition: opacity 0.2s; } +/* Delete button for trigger word */ +.delete-trigger-word-btn { + position: absolute; + top: -5px; + right: -5px; + width: 16px; + height: 16px; + background: var(--lora-error); + color: white; + border: none; + border-radius: 50%; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + transition: transform 0.2s ease; +} + +.delete-trigger-word-btn:hover { + transform: scale(1.1); +} + +/* Edit controls */ +.trigger-words-edit-controls { + display: flex; + justify-content: flex-end; + gap: var(--space-2); + margin-top: var(--space-2); +} + +.trigger-words-edit-controls button { + padding: 3px 8px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + font-size: 0.85em; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + transition: all 0.2s ease; +} + +.trigger-words-edit-controls button:hover { + background: oklch(var(--lora-accent) / 0.1); + border-color: var(--lora-accent); +} + +.trigger-words-edit-controls button i { + font-size: 0.8em; +} + +.save-trigger-words-btn { + background: var(--lora-accent) !important; + color: white !important; + border-color: var(--lora-accent) !important; +} + +.save-trigger-words-btn:hover { + opacity: 0.9; +} + +/* Add trigger word form */ +.add-trigger-word-form { + margin-top: var(--space-2); + display: flex; + gap: var(--space-1); +} + +.new-trigger-word-input { + flex: 1; + padding: 4px 8px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + font-size: 0.9em; +} + +.new-trigger-word-input:focus { + border-color: var(--lora-accent); + outline: none; +} + +.confirm-add-trigger-word-btn, +.cancel-add-trigger-word-btn { + padding: 4px 8px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + font-size: 0.85em; + cursor: pointer; + transition: all 0.2s ease; +} + +.confirm-add-trigger-word-btn { + background: var(--lora-accent); + color: white; + border-color: var(--lora-accent); +} + +.confirm-add-trigger-word-btn:hover { + opacity: 0.9; +} + +.cancel-add-trigger-word-btn:hover { + background: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .cancel-add-trigger-word-btn:hover { + background: rgba(255, 255, 255, 0.05); +} + /* Editable Fields */ .editable-field { position: relative; @@ -724,7 +887,7 @@ } .model-description-content blockquote { - border-left: 3px solid var(--lora-accent); + border-left: 3px solid var (--lora-accent); padding-left: 1em; margin-left: 0; margin-right: 0; diff --git a/static/js/components/LoraModal.js b/static/js/components/LoraModal.js index 49eaf368..2567e752 100644 --- a/static/js/components/LoraModal.js +++ b/static/js/components/LoraModal.js @@ -67,7 +67,7 @@ export function showLoraModal(lora) { - ${renderTriggerWords(escapedWords)} + ${renderTriggerWords(escapedWords, lora.file_path)}
@@ -120,6 +120,7 @@ export function showLoraModal(lora) { setupShowcaseScroll(); setupTabSwitching(); setupTagTooltip(); + setupTriggerWordsEditMode(); // If we have a model ID but no description, fetch it if (lora.civitai?.modelId && !lora.modelDescription) { @@ -488,35 +489,75 @@ async function saveModelMetadata(filePath, data) { } } -function renderTriggerWords(words) { +function renderTriggerWords(words, filePath) { if (!words.length) return `
- - No trigger word needed +
+ + +
+
+ No trigger word needed + +
+ +
`; return `
- -
- ${words.map(word => ` -
- ${word} - - - -
- `).join('')} +
+ + +
+
+
+ ${words.map(word => ` +
+ ${word} + + + + +
+ `).join('')} +
+
+ +
`; } -function renderShowcaseImages(images) { - return renderShowcaseContent(images); -} - export function toggleShowcase(element) { const carousel = element.nextElementSibling; const isCollapsed = carousel.classList.contains('collapsed'); @@ -738,4 +779,243 @@ function setupTagTooltip() { tooltip.classList.remove('visible'); }); } -} \ No newline at end of file +} + +// 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 && loraCard.dataset.civitai) { + const civitaiData = JSON.parse(loraCard.dataset.civitai); + civitaiData.trainedWords = words; + loraCard.dataset.civitai = JSON.stringify(civitaiData); + } + + // 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) { + 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'); + } +}; \ No newline at end of file