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) {
|
||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||
regenerateBtn.disabled = true;
|
||||
regenerateBtn.classList.add('loading');
|
||||
|
||||
try {
|
||||
// Get the last user message
|
||||
@@ -479,6 +480,7 @@ async function handleRegenerateMessage(messageDiv) {
|
||||
} catch (error) {
|
||||
console.error('Failed to regenerate message:', error);
|
||||
regenerateBtn.disabled = false;
|
||||
regenerateBtn.classList.remove('loading');
|
||||
addMessage(`Error regenerating message: ${error}`, false);
|
||||
}
|
||||
}
|
||||
@@ -521,11 +523,17 @@ async function generateSwipeNonStream(messageDiv, userMessage) {
|
||||
setStatus('Regeneration complete', 'success');
|
||||
setTimeout(() => setStatus('Ready'), 2000);
|
||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
||||
if (regenerateBtn) {
|
||||
regenerateBtn.disabled = false;
|
||||
regenerateBtn.classList.remove('loading');
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -564,7 +572,10 @@ async function generateSwipeStream(messageDiv, userMessage) {
|
||||
setStatus('Regeneration complete', 'success');
|
||||
setTimeout(() => setStatus('Ready'), 2000);
|
||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
||||
if (regenerateBtn) {
|
||||
regenerateBtn.disabled = false;
|
||||
regenerateBtn.classList.remove('loading');
|
||||
}
|
||||
tokenUnlisten();
|
||||
completeUnlisten();
|
||||
});
|
||||
@@ -576,7 +587,10 @@ async function generateSwipeStream(messageDiv, userMessage) {
|
||||
completeUnlisten();
|
||||
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -867,14 +881,18 @@ async function handleValidate() {
|
||||
}
|
||||
|
||||
validateBtn.disabled = true;
|
||||
validateBtn.classList.add('loading');
|
||||
validateBtn.textContent = 'Validating...';
|
||||
validationMsg.style.display = 'none';
|
||||
setStatus('Validating API...', 'default');
|
||||
|
||||
try {
|
||||
const models = await invoke('validate_api', { baseUrl, apiKey });
|
||||
|
||||
validationMsg.textContent = `Found ${models.length} models`;
|
||||
validationMsg.className = 'validation-message success';
|
||||
setStatus('API validated successfully', 'success');
|
||||
setTimeout(() => setStatus('Ready'), 2000);
|
||||
|
||||
modelSelect.innerHTML = '<option value="">Select a model</option>';
|
||||
models.forEach(model => {
|
||||
@@ -885,14 +903,17 @@ async function handleValidate() {
|
||||
});
|
||||
|
||||
modelsGroup.style.display = 'flex';
|
||||
modelsGroup.classList.add('fade-in');
|
||||
saveBtn.disabled = false;
|
||||
} catch (error) {
|
||||
validationMsg.textContent = `Validation failed: ${error}`;
|
||||
validationMsg.className = 'validation-message error';
|
||||
setStatus('API validation failed', 'error');
|
||||
modelsGroup.style.display = 'none';
|
||||
saveBtn.disabled = true;
|
||||
} finally {
|
||||
validateBtn.disabled = false;
|
||||
validateBtn.classList.remove('loading');
|
||||
validateBtn.textContent = 'Validate';
|
||||
}
|
||||
}
|
||||
@@ -914,6 +935,7 @@ async function handleSaveSettings(e) {
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.classList.add('loading');
|
||||
saveBtn.textContent = 'Saving...';
|
||||
setStatus('Saving configuration...', 'default');
|
||||
|
||||
@@ -935,6 +957,7 @@ async function handleSaveSettings(e) {
|
||||
setStatus('Failed to save configuration', 'error');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.classList.remove('loading');
|
||||
saveBtn.textContent = 'Save Configuration';
|
||||
}
|
||||
}
|
||||
@@ -1292,6 +1315,7 @@ async function handleSaveCharacter(e) {
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.classList.add('loading');
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
@@ -1324,6 +1348,7 @@ async function handleSaveCharacter(e) {
|
||||
characterMsg.className = 'validation-message error';
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.classList.remove('loading');
|
||||
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 {
|
||||
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 */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user