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:
64
src/main.js
64
src/main.js
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user