feat: add better loading states and animations
- Added spinning loading indicators on all buttons - Fade-in animations for newly revealed elements - Loading states with visual feedback (opacity, cursor) - Smooth CSS animations for spinners and fades - Loading indicators on: API validation, save operations, regenerate buttons, and character operations - Improved UX with clear visual feedback during async operations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
33
src/main.js
33
src/main.js
@@ -469,6 +469,7 @@ async function handleEditMessage(messageDiv, originalContent) {
|
|||||||
async function handleRegenerateMessage(messageDiv) {
|
async function handleRegenerateMessage(messageDiv) {
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
regenerateBtn.disabled = true;
|
regenerateBtn.disabled = true;
|
||||||
|
regenerateBtn.classList.add('loading');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the last user message
|
// Get the last user message
|
||||||
@@ -479,6 +480,7 @@ async function handleRegenerateMessage(messageDiv) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to regenerate message:', error);
|
console.error('Failed to regenerate message:', error);
|
||||||
regenerateBtn.disabled = false;
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
addMessage(`Error regenerating message: ${error}`, false);
|
addMessage(`Error regenerating message: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,11 +523,17 @@ async function generateSwipeNonStream(messageDiv, userMessage) {
|
|||||||
setStatus('Regeneration complete', 'success');
|
setStatus('Regeneration complete', 'success');
|
||||||
setTimeout(() => setStatus('Ready'), 2000);
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
addMessage(`Error regenerating message: ${error}`, false);
|
addMessage(`Error regenerating message: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +572,10 @@ async function generateSwipeStream(messageDiv, userMessage) {
|
|||||||
setStatus('Regeneration complete', 'success');
|
setStatus('Regeneration complete', 'success');
|
||||||
setTimeout(() => setStatus('Ready'), 2000);
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
tokenUnlisten();
|
tokenUnlisten();
|
||||||
completeUnlisten();
|
completeUnlisten();
|
||||||
});
|
});
|
||||||
@@ -576,7 +587,10 @@ async function generateSwipeStream(messageDiv, userMessage) {
|
|||||||
completeUnlisten();
|
completeUnlisten();
|
||||||
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
addMessage(`Error: ${error}`, false);
|
addMessage(`Error: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -867,14 +881,18 @@ async function handleValidate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateBtn.disabled = true;
|
validateBtn.disabled = true;
|
||||||
|
validateBtn.classList.add('loading');
|
||||||
validateBtn.textContent = 'Validating...';
|
validateBtn.textContent = 'Validating...';
|
||||||
validationMsg.style.display = 'none';
|
validationMsg.style.display = 'none';
|
||||||
|
setStatus('Validating API...', 'default');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const models = await invoke('validate_api', { baseUrl, apiKey });
|
const models = await invoke('validate_api', { baseUrl, apiKey });
|
||||||
|
|
||||||
validationMsg.textContent = `Found ${models.length} models`;
|
validationMsg.textContent = `Found ${models.length} models`;
|
||||||
validationMsg.className = 'validation-message success';
|
validationMsg.className = 'validation-message success';
|
||||||
|
setStatus('API validated successfully', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
|
|
||||||
modelSelect.innerHTML = '<option value="">Select a model</option>';
|
modelSelect.innerHTML = '<option value="">Select a model</option>';
|
||||||
models.forEach(model => {
|
models.forEach(model => {
|
||||||
@@ -885,14 +903,17 @@ async function handleValidate() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
modelsGroup.style.display = 'flex';
|
modelsGroup.style.display = 'flex';
|
||||||
|
modelsGroup.classList.add('fade-in');
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
validationMsg.textContent = `Validation failed: ${error}`;
|
validationMsg.textContent = `Validation failed: ${error}`;
|
||||||
validationMsg.className = 'validation-message error';
|
validationMsg.className = 'validation-message error';
|
||||||
|
setStatus('API validation failed', 'error');
|
||||||
modelsGroup.style.display = 'none';
|
modelsGroup.style.display = 'none';
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
} finally {
|
} finally {
|
||||||
validateBtn.disabled = false;
|
validateBtn.disabled = false;
|
||||||
|
validateBtn.classList.remove('loading');
|
||||||
validateBtn.textContent = 'Validate';
|
validateBtn.textContent = 'Validate';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -914,6 +935,7 @@ async function handleSaveSettings(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.classList.add('loading');
|
||||||
saveBtn.textContent = 'Saving...';
|
saveBtn.textContent = 'Saving...';
|
||||||
setStatus('Saving configuration...', 'default');
|
setStatus('Saving configuration...', 'default');
|
||||||
|
|
||||||
@@ -935,6 +957,7 @@ async function handleSaveSettings(e) {
|
|||||||
setStatus('Failed to save configuration', 'error');
|
setStatus('Failed to save configuration', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.classList.remove('loading');
|
||||||
saveBtn.textContent = 'Save Configuration';
|
saveBtn.textContent = 'Save Configuration';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1292,6 +1315,7 @@ async function handleSaveCharacter(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.classList.add('loading');
|
||||||
saveBtn.textContent = 'Saving...';
|
saveBtn.textContent = 'Saving...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1324,6 +1348,7 @@ async function handleSaveCharacter(e) {
|
|||||||
characterMsg.className = 'validation-message error';
|
characterMsg.className = 'validation-message error';
|
||||||
} finally {
|
} finally {
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.classList.remove('loading');
|
||||||
saveBtn.textContent = 'Save Character';
|
saveBtn.textContent = 'Save Character';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,6 +246,19 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.message.user {
|
.message.user {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
@@ -757,6 +770,47 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: currentColor;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button loading state */
|
||||||
|
.btn-primary.loading,
|
||||||
|
.btn-secondary.loading,
|
||||||
|
.message-action-btn.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: wait;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary.loading::after,
|
||||||
|
.btn-secondary.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: white;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* Light mode support */
|
/* Light mode support */
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
Reference in New Issue
Block a user