From cbb25b4ac0de4433283170158d0619f8c625f8de Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Fri, 30 May 2025 16:30:01 +0800 Subject: [PATCH] Enhance model metadata saving functionality with loading indicators and improved validation. Refactor editing logic for better user experience in both checkpoint and LoRA modals. Fixes #200 --- static/js/api/checkpointApi.js | 36 +++-- static/js/api/loraApi.js | 36 +++-- .../checkpointModal/ModelMetadata.js | 133 ++++++++++-------- static/js/components/checkpointModal/index.js | 2 +- .../js/components/loraModal/ModelMetadata.js | 91 +++++++++--- static/js/components/loraModal/index.js | 2 +- 6 files changed, 192 insertions(+), 108 deletions(-) diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js index 957ad42c..c7bcab6c 100644 --- a/static/js/api/checkpointApi.js +++ b/static/js/api/checkpointApi.js @@ -120,22 +120,30 @@ export async function refreshSingleCheckpointMetadata(filePath) { * @returns {Promise} - Promise that resolves with the server response */ export async function saveModelMetadata(filePath, data) { - const response = await fetch('/api/checkpoints/save-metadata', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath, - ...data - }) - }); + try { + // Show loading indicator + state.loadingManager.showSimpleLoading('Saving metadata...'); + + const response = await fetch('/api/checkpoints/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'); + if (!response.ok) { + throw new Error('Failed to save metadata'); + } + + return response.json(); + } finally { + // Always hide the loading indicator when done + state.loadingManager.hide(); } - - return response.json(); } /** diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 60ac962f..e9b13df0 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -21,22 +21,30 @@ import { state, getCurrentPageState } from '../state/index.js'; * @returns {Promise} Promise of the save operation */ export 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 - }) - }); + try { + // Show loading indicator + state.loadingManager.showSimpleLoading('Saving metadata...'); + + 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'); + if (!response.ok) { + throw new Error('Failed to save metadata'); + } + + return response.json(); + } finally { + // Always hide the loading indicator when done + state.loadingManager.hide(); } - - return response.json(); } /** diff --git a/static/js/components/checkpointModal/ModelMetadata.js b/static/js/components/checkpointModal/ModelMetadata.js index cc3f4046..f3964208 100644 --- a/static/js/components/checkpointModal/ModelMetadata.js +++ b/static/js/components/checkpointModal/ModelMetadata.js @@ -17,6 +17,9 @@ export function setupModelNameEditing(filePath) { if (!modelNameContent || !editBtn) return; + // Store the file path in a data attribute for later use + modelNameContent.dataset.filePath = filePath; + // Show edit button on hover const modelNameHeader = document.querySelector('.model-name-header'); modelNameHeader.addEventListener('mouseenter', () => { @@ -24,14 +27,17 @@ export function setupModelNameEditing(filePath) { }); modelNameHeader.addEventListener('mouseleave', () => { - if (!modelNameContent.getAttribute('data-editing')) { + if (!modelNameHeader.classList.contains('editing')) { editBtn.classList.remove('visible'); } }); // Handle edit button click editBtn.addEventListener('click', () => { - modelNameContent.setAttribute('data-editing', 'true'); + modelNameHeader.classList.add('editing'); + modelNameContent.setAttribute('contenteditable', 'true'); + // Store original value for comparison later + modelNameContent.dataset.originalValue = modelNameContent.textContent.trim(); modelNameContent.focus(); // Place cursor at the end @@ -47,33 +53,25 @@ export function setupModelNameEditing(filePath) { 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 - // Use the passed filePath to find the card - const checkpointCard = document.querySelector(`.checkpoint-card[data-filepath="${filePath}"]`); - if (checkpointCard) { - this.textContent = checkpointCard.dataset.model_name; - } - } - }); - - // Handle enter key + // Handle keyboard events in edit mode modelNameContent.addEventListener('keydown', function(e) { + if (!this.getAttribute('contenteditable')) return; + if (e.key === 'Enter') { e.preventDefault(); - // Use the passed filePath - saveModelName(filePath); - this.blur(); + this.blur(); // Trigger save on Enter + } else if (e.key === 'Escape') { + e.preventDefault(); + // Restore original value + this.textContent = this.dataset.originalValue; + exitEditMode(); } }); // Limit model name length modelNameContent.addEventListener('input', function() { + if (!this.getAttribute('contenteditable')) return; + if (this.textContent.length > 100) { this.textContent = this.textContent.substring(0, 100); // Place cursor at the end @@ -87,44 +85,59 @@ export function setupModelNameEditing(filePath) { showToast('Model name is limited to 100 characters', 'warning'); } }); -} - -/** - * Save model name - * @param {string} filePath - File path - */ -async function saveModelName(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; - } + // Handle focus out - save changes + modelNameContent.addEventListener('blur', async function() { + if (!this.getAttribute('contenteditable')) return; + + const newModelName = this.textContent.trim(); + const originalValue = this.dataset.originalValue; + + // Basic validation + if (!newModelName) { + // Restore original value if empty + this.textContent = originalValue; + showToast('Model name cannot be empty', 'error'); + exitEditMode(); + return; + } + + if (newModelName === originalValue) { + // No changes, just exit edit mode + exitEditMode(); + return; + } + + try { + // Get the file path from the dataset + const filePath = this.dataset.filePath; + + await saveModelMetadata(filePath, { model_name: newModelName }); + + // Update the corresponding checkpoint card's dataset and display + updateCheckpointCard(filePath, { model_name: newModelName }); + + // BUGFIX: Directly update the card's dataset.name attribute to ensure + // it's correctly read when reopening the modal + const checkpointCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); + if (checkpointCard) { + checkpointCard.dataset.name = newModelName; + } + + showToast('Model name updated successfully', 'success'); + } catch (error) { + console.error('Error updating model name:', error); + this.textContent = originalValue; // Restore original model name + showToast('Failed to update model name', 'error'); + } finally { + exitEditMode(); + } + }); - // Check if model name is too long - 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 card with the new model name - updateCheckpointCard(filePath, { name: newModelName }); - - showToast('Model name updated successfully', 'success'); - - // No need to reload the entire page - // setTimeout(() => { - // window.location.reload(); - // }, 1500); - } catch (error) { - showToast('Failed to update model name', 'error'); + function exitEditMode() { + modelNameContent.removeAttribute('contenteditable'); + modelNameHeader.classList.remove('editing'); + editBtn.classList.remove('visible'); } } @@ -138,6 +151,9 @@ export function setupBaseModelEditing(filePath) { if (!baseModelContent || !editBtn) return; + // Store the file path in a data attribute for later use + baseModelContent.dataset.filePath = filePath; + // Show edit button on hover const baseModelDisplay = document.querySelector('.base-model-display'); baseModelDisplay.addEventListener('mouseenter', () => { @@ -303,6 +319,9 @@ export function setupFileNameEditing(filePath) { if (!fileNameContent || !editBtn) return; + // Store the original file path + fileNameContent.dataset.filePath = filePath; + // Show edit button on hover const fileNameWrapper = document.querySelector('.file-name-wrapper'); fileNameWrapper.addEventListener('mouseenter', () => { diff --git a/static/js/components/checkpointModal/index.js b/static/js/components/checkpointModal/index.js index 7df41fd0..bfa7b51f 100644 --- a/static/js/components/checkpointModal/index.js +++ b/static/js/components/checkpointModal/index.js @@ -27,7 +27,7 @@ export function showCheckpointModal(checkpoint) {