feat: improve status feedback with detailed messages and colors

- Added color-coded status indicators (success, error, streaming)
- Detailed status messages for all operations
- Success messages auto-reset to "Ready" after 2 seconds
- Error messages show context for debugging
- Status feedback for: API calls, character switching, history clearing,
  configuration saving, message generation, and regeneration
- Smooth color transitions for better UX

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-14 12:09:57 -07:00
parent e0239aceda
commit 4866c11245
2 changed files with 53 additions and 20 deletions

View File

@@ -485,7 +485,7 @@ async function handleRegenerateMessage(messageDiv) {
// Generate a new swipe for an existing assistant message // Generate a new swipe for an existing assistant message
async function generateSwipe(messageDiv, userMessage) { async function generateSwipe(messageDiv, userMessage) {
setStatus('Regenerating...'); setStatus('Regenerating response...', 'default');
// Check if streaming is enabled // Check if streaming is enabled
let streamEnabled = false; let streamEnabled = false;
@@ -518,11 +518,12 @@ async function generateSwipeNonStream(messageDiv, userMessage) {
// Update swipe controls // Update swipe controls
updateSwipeControls(messageDiv, swipeInfo.current, swipeInfo.total); updateSwipeControls(messageDiv, swipeInfo.current, swipeInfo.total);
setStatus('Ready'); setStatus('Regeneration complete', 'success');
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;
} catch (error) { } catch (error) {
setStatus('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;
addMessage(`Error regenerating message: ${error}`, false); addMessage(`Error regenerating message: ${error}`, false);
@@ -531,8 +532,7 @@ async function generateSwipeNonStream(messageDiv, userMessage) {
// Generate swipe using streaming // Generate swipe using streaming
async function generateSwipeStream(messageDiv, userMessage) { async function generateSwipeStream(messageDiv, userMessage) {
setStatus('Streaming...'); setStatus('Streaming regeneration...', 'streaming');
statusText.classList.add('streaming');
let fullContent = ''; let fullContent = '';
const contentDiv = messageDiv.querySelector('.message-content'); const contentDiv = messageDiv.querySelector('.message-content');
@@ -561,8 +561,8 @@ async function generateSwipeStream(messageDiv, userMessage) {
console.error('Failed to add swipe:', error); console.error('Failed to add swipe:', error);
} }
setStatus('Ready'); setStatus('Regeneration complete', 'success');
statusText.classList.remove('streaming'); 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;
tokenUnlisten(); tokenUnlisten();
@@ -574,8 +574,7 @@ async function generateSwipeStream(messageDiv, userMessage) {
} catch (error) { } catch (error) {
tokenUnlisten(); tokenUnlisten();
completeUnlisten(); completeUnlisten();
statusText.classList.remove('streaming'); setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
setStatus('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;
addMessage(`Error: ${error}`, false); addMessage(`Error: ${error}`, false);
@@ -620,7 +619,7 @@ async function sendMessage(message, isRegenerate = false) {
sendBtn.disabled = true; sendBtn.disabled = true;
messageInput.disabled = true; messageInput.disabled = true;
setStatus('Thinking...'); setStatus('Connecting to API...', 'default');
// Check if streaming is enabled // Check if streaming is enabled
let streamEnabled = false; let streamEnabled = false;
@@ -633,8 +632,7 @@ async function sendMessage(message, isRegenerate = false) {
if (streamEnabled) { if (streamEnabled) {
// Use streaming // Use streaming
setStatus('Streaming...'); setStatus('Streaming response...', 'streaming');
statusText.classList.add('streaming');
let streamingMessageDiv = null; let streamingMessageDiv = null;
let streamingContentDiv = null; let streamingContentDiv = null;
@@ -705,8 +703,8 @@ async function sendMessage(message, isRegenerate = false) {
actionsDiv.appendChild(regenerateBtn); actionsDiv.appendChild(regenerateBtn);
streamingMessageDiv.appendChild(actionsDiv); streamingMessageDiv.appendChild(actionsDiv);
setStatus('Ready'); setStatus('Response complete', 'success');
statusText.classList.remove('streaming'); setTimeout(() => setStatus('Ready'), 2000);
sendBtn.disabled = false; sendBtn.disabled = false;
messageInput.disabled = false; messageInput.disabled = false;
messageInput.focus(); messageInput.focus();
@@ -719,17 +717,17 @@ async function sendMessage(message, isRegenerate = false) {
} catch (error) { } catch (error) {
tokenUnlisten(); tokenUnlisten();
completeUnlisten(); completeUnlisten();
statusText.classList.remove('streaming');
if (streamingMessageDiv) { if (streamingMessageDiv) {
streamingMessageDiv.remove(); streamingMessageDiv.remove();
} }
if (error.includes('not configured')) { if (error.includes('not configured')) {
addMessage('API not configured. Please configure your API settings.', false); addMessage('API not configured. Please configure your API settings.', false);
setStatus('API not configured', 'error');
setTimeout(showSettings, 1000); setTimeout(showSettings, 1000);
} else { } else {
addMessage(`Error: ${error}`, false); addMessage(`Error: ${error}`, false);
setStatus(`Error: ${error.substring(0, 50)}...`, 'error');
} }
setStatus('Error');
sendBtn.disabled = false; sendBtn.disabled = false;
messageInput.disabled = false; messageInput.disabled = false;
messageInput.focus(); messageInput.focus();
@@ -742,16 +740,18 @@ async function sendMessage(message, isRegenerate = false) {
const response = await invoke('chat', { message }); const response = await invoke('chat', { message });
removeTypingIndicator(); removeTypingIndicator();
addMessage(response, false); addMessage(response, false);
setStatus('Ready'); setStatus('Response complete', 'success');
setTimeout(() => setStatus('Ready'), 2000);
} catch (error) { } catch (error) {
removeTypingIndicator(); removeTypingIndicator();
if (error.includes('not configured')) { if (error.includes('not configured')) {
addMessage('API not configured. Please configure your API settings.', false); addMessage('API not configured. Please configure your API settings.', false);
setStatus('API not configured', 'error');
setTimeout(showSettings, 1000); setTimeout(showSettings, 1000);
} else { } else {
addMessage(`Error: ${error}`, false); addMessage(`Error: ${error}`, false);
setStatus(`Error: ${error.substring(0, 50)}...`, 'error');
} }
setStatus('Error');
} finally { } finally {
sendBtn.disabled = false; sendBtn.disabled = false;
messageInput.disabled = false; messageInput.disabled = false;
@@ -788,9 +788,21 @@ function removeTypingIndicator() {
} }
} }
// Update status // Update status with optional styling
function setStatus(text) { function setStatus(text, type = 'default') {
statusText.textContent = text; statusText.textContent = text;
// Remove all status classes
statusText.classList.remove('streaming', 'error', 'success');
// Add appropriate class based on type
if (type === 'streaming') {
statusText.classList.add('streaming');
} else if (type === 'error') {
statusText.classList.add('error');
} else if (type === 'success') {
statusText.classList.add('success');
}
} }
// Show/hide settings // Show/hide settings
@@ -903,20 +915,24 @@ async function handleSaveSettings(e) {
saveBtn.disabled = true; saveBtn.disabled = true;
saveBtn.textContent = 'Saving...'; saveBtn.textContent = 'Saving...';
setStatus('Saving configuration...', 'default');
try { try {
await invoke('save_api_config', { baseUrl, apiKey, model, stream }); await invoke('save_api_config', { baseUrl, apiKey, model, stream });
validationMsg.textContent = 'Configuration saved successfully'; validationMsg.textContent = 'Configuration saved successfully';
validationMsg.className = 'validation-message success'; validationMsg.className = 'validation-message success';
setStatus('Configuration saved', 'success');
setTimeout(() => { setTimeout(() => {
hideSettings(); hideSettings();
messagesContainer.innerHTML = ''; messagesContainer.innerHTML = '';
addMessage('API configured. Ready to chat.', false, true); addMessage('API configured. Ready to chat.', false, true);
setStatus('Ready');
}, 1000); }, 1000);
} catch (error) { } catch (error) {
validationMsg.textContent = `Failed to save: ${error}`; validationMsg.textContent = `Failed to save: ${error}`;
validationMsg.className = 'validation-message error'; validationMsg.className = 'validation-message error';
setStatus('Failed to save configuration', 'error');
} finally { } finally {
saveBtn.disabled = false; saveBtn.disabled = false;
saveBtn.textContent = 'Save Configuration'; saveBtn.textContent = 'Save Configuration';
@@ -1048,12 +1064,16 @@ async function loadCharacters() {
// Handle character switching // Handle character switching
async function handleCharacterSwitch() { async function handleCharacterSwitch() {
const characterId = characterSelect.value; const characterId = characterSelect.value;
setStatus('Switching character...', 'default');
try { try {
await invoke('set_active_character', { characterId }); await invoke('set_active_character', { characterId });
messagesContainer.innerHTML = ''; messagesContainer.innerHTML = '';
await loadCharacters(); await loadCharacters();
setStatus('Character switched', 'success');
setTimeout(() => setStatus('Ready'), 2000);
} catch (error) { } catch (error) {
console.error('Failed to switch character:', error); console.error('Failed to switch character:', error);
setStatus('Failed to switch character', 'error');
addMessage(`Failed to switch character: ${error}`, false); addMessage(`Failed to switch character: ${error}`, false);
} }
} }
@@ -1175,6 +1195,7 @@ async function clearHistory() {
return; return;
} }
setStatus('Clearing history...', 'default');
try { try {
await invoke('clear_chat_history'); await invoke('clear_chat_history');
messagesContainer.innerHTML = ''; messagesContainer.innerHTML = '';
@@ -1183,7 +1204,10 @@ async function clearHistory() {
} else { } else {
addMessage('Conversation cleared. Ready to chat.', false, true); addMessage('Conversation cleared. Ready to chat.', false, true);
} }
setStatus('History cleared', 'success');
setTimeout(() => setStatus('Ready'), 2000);
} catch (error) { } catch (error) {
setStatus('Failed to clear history', 'error');
addMessage(`Failed to clear history: ${error}`, false); addMessage(`Failed to clear history: ${error}`, false);
} }
} }

View File

@@ -732,6 +732,7 @@ body {
.status-text { .status-text {
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
transition: color 0.2s ease;
} }
.status-text.streaming { .status-text.streaming {
@@ -739,6 +740,14 @@ body {
animation: pulse 2s ease-in-out infinite; animation: pulse 2s ease-in-out infinite;
} }
.status-text.error {
color: #ef4444;
}
.status-text.success {
color: #22c55e;
}
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%, 100% {
opacity: 1; opacity: 1;