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) {