From dc728a5a94c29c66b0351f48f954e0db7eaa1ca1 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 18 Jan 2026 20:22:15 +0000
Subject: [PATCH] feat(ux): add contextual loading state to generate button
Replaces the full-screen loading overlay with a spinner inside the "Generate Poster" button.
This provides less disruptive feedback to the user and prevents multiple submissions. The button is disabled, its text content is updated, and an `aria-label` is added for accessibility during the loading state.
---
web_app/static/index.html | 5 ++++-
web_app/static/script.js | 29 +++++++++++++++++++++++------
web_app/static/style.css | 22 ++++++++++++++++++++++
3 files changed, 49 insertions(+), 7 deletions(-)
diff --git a/web_app/static/index.html b/web_app/static/index.html
index 0d56371..a9e2343 100644
--- a/web_app/static/index.html
+++ b/web_app/static/index.html
@@ -116,7 +116,10 @@
Lyrics
- Generate Poster
+
+ Generate Poster
+
+
diff --git a/web_app/static/script.js b/web_app/static/script.js
index 441a4ed..1d27fcb 100644
--- a/web_app/static/script.js
+++ b/web_app/static/script.js
@@ -613,7 +613,16 @@ function handleLyricLineClick(lineNumber) {
async function generatePoster() {
if (!currentMetadata) return;
- loadingOverlay.style.display = 'flex';
+ const btnText = generateBtn.querySelector('.btn-text');
+ const btnSpinner = generateBtn.querySelector('.button-spinner');
+
+ const resetButtonState = () => {
+ generateBtn.disabled = false;
+ generateBtn.classList.remove('loading');
+ if(btnText) btnText.textContent = 'Generate Poster';
+ if(btnSpinner) btnSpinner.style.display = 'none';
+ generateBtn.removeAttribute('aria-label');
+ };
const indexingToggle = document.getElementById('indexingToggle');
const accentToggle = document.getElementById('accentToggle');
@@ -628,14 +637,12 @@ async function generatePoster() {
};
if (payload.type === 'track') {
- // Lyrics Logic (Same as before)
const startVal = parseInt(startLineInput.value) || 1;
const endVal = parseInt(endLineInput.value) || 1;
const start = startVal - 1;
const end = endVal;
if (isNaN(start) || isNaN(end) || start >= end) {
- loadingOverlay.style.display = 'none';
showToast("Please enter a valid range (Start must be less than End).", "error");
return;
}
@@ -644,10 +651,16 @@ async function generatePoster() {
payload.lyrics_end = end;
} else {
- // Album Logic
payload.indexing = indexingToggle ? indexingToggle.checked : false;
}
+ // Set loading state AFTER validation
+ generateBtn.disabled = true;
+ generateBtn.classList.add('loading');
+ if(btnText) btnText.textContent = 'Generating...';
+ if(btnSpinner) btnSpinner.style.display = 'block';
+ generateBtn.setAttribute('aria-label', 'Generating poster, please wait');
+
try {
const response = await fetch('/api/generate', {
method: 'POST',
@@ -669,13 +682,17 @@ async function generatePoster() {
posterContainer.innerHTML = '';
posterContainer.appendChild(img);
showDownloadButton(imageUrl, data.filename);
- loadingOverlay.style.display = 'none';
+ resetButtonState();
+ };
+ img.onerror = () => {
+ showToast('Error: Failed to load the generated poster image.', 'error');
+ resetButtonState();
};
} catch (error) {
console.error("Generation failed", error);
showToast(`Error: ${error.message}`, "error");
- loadingOverlay.style.display = 'none';
+ resetButtonState();
}
}
diff --git a/web_app/static/style.css b/web_app/static/style.css
index 468eb76..a63b9bb 100644
--- a/web_app/static/style.css
+++ b/web_app/static/style.css
@@ -166,6 +166,28 @@ header p {
transform: translateY(-1px);
}
+.primary-btn:disabled {
+ background: var(--surface-elevated);
+ color: var(--text-muted);
+ cursor: not-allowed;
+ transform: none;
+}
+
+.primary-btn.loading .btn-text {
+ visibility: hidden;
+ opacity: 0;
+}
+
+.button-spinner {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border: 2px solid rgba(0, 0, 0, 0.2);
+ border-top-color: var(--surface-base);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
.secondary-btn {
background: transparent;
border-color: var(--border-color);