diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index 35a988d2..0770561d 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -183,7 +183,11 @@ outline: none; } -.edit-file-name-btn { +/* 合并编辑按钮样式 */ +.edit-model-name-btn, +.edit-file-name-btn, +.edit-base-model-btn, +.edit-model-description-btn { background: transparent; border: none; color: var(--text-color); @@ -195,17 +199,28 @@ margin-left: var(--space-1); } +.edit-model-name-btn.visible, .edit-file-name-btn.visible, -.file-name-wrapper:hover .edit-file-name-btn { +.edit-base-model-btn.visible, +.edit-model-description-btn.visible, +.model-name-header:hover .edit-model-name-btn, +.file-name-wrapper:hover .edit-file-name-btn, +.base-model-display:hover .edit-base-model-btn, +.model-name-header:hover .edit-model-description-btn { opacity: 0.5; } -.edit-file-name-btn:hover { +.edit-model-name-btn:hover, +.edit-file-name-btn:hover, +.edit-base-model-btn:hover, +.edit-model-description-btn:hover { opacity: 0.8 !important; background: rgba(0, 0, 0, 0.05); } -[data-theme="dark"] .edit-file-name-btn:hover { +[data-theme="dark"] .edit-model-name-btn:hover, +[data-theme="dark"] .edit-file-name-btn:hover, +[data-theme="dark"] .edit-base-model-btn:hover { background: rgba(255, 255, 255, 0.05); } @@ -234,32 +249,6 @@ flex: 1; } -.edit-base-model-btn { - background: transparent; - border: none; - color: var(--text-color); - opacity: 0; - cursor: pointer; - padding: 2px 5px; - border-radius: var(--border-radius-xs); - transition: all 0.2s ease; - margin-left: var(--space-1); -} - -.edit-base-model-btn.visible, -.base-model-display:hover .edit-base-model-btn { - opacity: 0.5; -} - -.edit-base-model-btn:hover { - opacity: 0.8 !important; - background: rgba(0, 0, 0, 0.05); -} - -[data-theme="dark"] .edit-base-model-btn:hover { - background: rgba(255, 255, 255, 0.05); -} - .base-model-selector { width: 100%; padding: 3px 5px; @@ -316,32 +305,6 @@ background: var(--bg-color); } -.edit-model-name-btn { - background: transparent; - border: none; - color: var(--text-color); - opacity: 0; - cursor: pointer; - padding: 2px 5px; - border-radius: var(--border-radius-xs); - transition: all 0.2s ease; - margin-left: var(--space-1); -} - -.edit-model-name-btn.visible, -.model-name-header:hover .edit-model-name-btn { - opacity: 0.5; -} - -.edit-model-name-btn:hover { - opacity: 0.8 !important; - background: rgba(0, 0, 0, 0.05); -} - -[data-theme="dark"] .edit-model-name-btn:hover { - background: rgba(255, 255, 255, 0.05); -} - /* Tab System Styling */ .showcase-tabs { display: flex; diff --git a/static/js/components/shared/ModelDescription.js b/static/js/components/shared/ModelDescription.js index a7179805..a2297e01 100644 --- a/static/js/components/shared/ModelDescription.js +++ b/static/js/components/shared/ModelDescription.js @@ -1,3 +1,5 @@ +import { showToast } from '../../utils/uiHelpers.js'; + /** * ModelDescription.js * Handles model description related functionality - General version @@ -40,4 +42,99 @@ export function setupTabSwitching() { } }); }); +} + +/** + * Set up model description editing functionality + * @param {string} filePath - File path + */ +export function setupModelDescriptionEditing(filePath) { + const descContent = document.querySelector('.model-description-content'); + const descContainer = document.querySelector('.model-description-container'); + if (!descContent || !descContainer) return; + + // Add edit button if not present + let editBtn = descContainer.querySelector('.edit-model-description-btn'); + if (!editBtn) { + editBtn = document.createElement('button'); + editBtn.className = 'edit-model-description-btn'; + editBtn.title = 'Edit model description'; + editBtn.innerHTML = ''; + descContainer.insertBefore(editBtn, descContent); + } + + // Show edit button on hover + descContainer.addEventListener('mouseenter', () => { + editBtn.classList.add('visible'); + }); + descContainer.addEventListener('mouseleave', () => { + if (!descContainer.classList.contains('editing')) { + editBtn.classList.remove('visible'); + } + }); + + // Handle edit button click + editBtn.addEventListener('click', () => { + descContainer.classList.add('editing'); + descContent.setAttribute('contenteditable', 'true'); + descContent.dataset.originalValue = descContent.innerHTML.trim(); + descContent.focus(); + + // Place cursor at the end + const range = document.createRange(); + const sel = window.getSelection(); + range.selectNodeContents(descContent); + range.collapse(false); + sel.removeAllRanges(); + sel.addRange(range); + + editBtn.classList.add('visible'); + }); + + // Keyboard events + descContent.addEventListener('keydown', function(e) { + if (!this.getAttribute('contenteditable')) return; + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.blur(); + } else if (e.key === 'Escape') { + e.preventDefault(); + this.innerHTML = this.dataset.originalValue; + exitEditMode(); + } + }); + + // Save on blur + descContent.addEventListener('blur', async function() { + if (!this.getAttribute('contenteditable')) return; + const newValue = this.innerHTML.trim(); + const originalValue = this.dataset.originalValue; + if (newValue === originalValue) { + exitEditMode(); + return; + } + if (!newValue) { + this.innerHTML = originalValue; + showToast('Description cannot be empty', 'error'); + exitEditMode(); + return; + } + try { + // Save to backend + const { getModelApiClient } = await import('../../api/baseModelApi.js'); + await getModelApiClient().saveModelMetadata(filePath, { modelDescription: newValue }); + showToast('Model description updated', 'success'); + } catch (err) { + this.innerHTML = originalValue; + showToast('Failed to update model description', 'error'); + } finally { + exitEditMode(); + } + }); + + function exitEditMode() { + descContent.removeAttribute('contenteditable'); + descContainer.classList.remove('editing'); + editBtn.classList.remove('visible'); + } } \ No newline at end of file diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index d6246487..72d6bbaf 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -6,7 +6,7 @@ import { scrollToTop, loadExampleImages } from './showcase/ShowcaseView.js'; -import { setupTabSwitching } from './ModelDescription.js'; +import { setupTabSwitching, setupModelDescriptionEditing } from './ModelDescription.js'; import { setupModelNameEditing, setupBaseModelEditing, @@ -33,7 +33,6 @@ export function showModelModal(model, modelType) { model.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : []; // Generate model type specific content - // const typeSpecificContent = modelType === 'loras' ? renderLoraSpecificContent(model, escapedWords) : ''; let typeSpecificContent; if (modelType === 'loras') { typeSpecificContent = renderLoraSpecificContent(model, escapedWords); @@ -211,6 +210,7 @@ export function showModelModal(model, modelType) { setupModelNameEditing(model.file_path); setupBaseModelEditing(model.file_path); setupFileNameEditing(model.file_path); + setupModelDescriptionEditing(model.file_path, model.modelDescription || ''); setupEventHandlers(model.file_path); // LoRA specific setup