Chat: Add draft auto-save to localStorage with restore and discard options

- Add draft management functions to app.js (saveDraft, loadDraft, clearDraft, getDraftTimestamp)
- Implement draft auto-save in chat that saves every 2 seconds after typing stops
- Show draft restoration banner on page load if draft exists and is less than 7 days old
- Display draft age with "X minutes/hours/days ago" format
- Add Restore and Discard buttons to draft indicator
- Clear draft automatically after successfully sending a message
- Add draft indicator styles matching beige/blue/pink theme with cyan restore button
- Support dark mode with appropriate color adjustments
- Draft indicator slides in from top with smooth animation

Draft key format: draft_chat_{boardId} for per-board draft storage.
jocadbz
Joca 2026-01-15 23:23:12 -03:00
parent 309e516480
commit e76049a353
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
3 changed files with 239 additions and 1 deletions

View File

@ -59,7 +59,6 @@ function enableEnterToSubmit(input, form) {
} }
}); });
}); });
});
} }
// Initialize on DOM ready // Initialize on DOM ready
@ -283,3 +282,92 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
}); });
}); });
// ============================================
// Draft Auto-Save Functions
// ============================================
// Save draft to localStorage
function saveDraft(key, content) {
try {
localStorage.setItem(key, content);
localStorage.setItem(key + '_timestamp', Date.now().toString());
} catch (e) {
console.error('Failed to save draft:', e);
}
}
// Load draft from localStorage
function loadDraft(key) {
try {
return localStorage.getItem(key);
} catch (e) {
console.error('Failed to load draft:', e);
return null;
}
}
// Clear draft from localStorage
function clearDraft(key) {
try {
localStorage.removeItem(key);
localStorage.removeItem(key + '_timestamp');
} catch (e) {
console.error('Failed to clear draft:', e);
}
}
// Get draft timestamp
function getDraftTimestamp(key) {
try {
const timestamp = localStorage.getItem(key + '_timestamp');
return timestamp ? parseInt(timestamp) : null;
} catch (e) {
console.error('Failed to get draft timestamp:', e);
return null;
}
}
// Format time ago
function formatTimeAgo(timestamp) {
const now = Date.now();
const diff = now - timestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
return 'just now';
}
// Show draft restoration indicator
function showDraftIndicator(content, timestamp, onRestore, onDiscard) {
const indicator = document.createElement('div');
indicator.className = 'draft-indicator';
indicator.innerHTML = `
<div class="draft-indicator-content">
<span class="draft-indicator-icon">📝</span>
<span class="draft-indicator-text">Draft from ${formatTimeAgo(timestamp)}</span>
<button type="button" class="draft-indicator-button restore">Restore</button>
<button type="button" class="draft-indicator-button discard">Discard</button>
</div>
`;
const restoreBtn = indicator.querySelector('.restore');
const discardBtn = indicator.querySelector('.discard');
restoreBtn.addEventListener('click', () => {
onRestore(content);
indicator.remove();
});
discardBtn.addEventListener('click', () => {
onDiscard();
indicator.remove();
});
return indicator;
}

View File

@ -687,6 +687,70 @@ input.error, textarea.error, select.error {
font-weight: bold; font-weight: bold;
} }
/* Draft indicator styles */
.draft-indicator {
background-color: #f3d2c1;
border: 1px solid #001858;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
animation: slideInFromTop 0.3s ease-out;
}
.draft-indicator-content {
display: flex;
align-items: center;
gap: 10px;
font-family: monospace;
}
.draft-indicator-icon {
font-size: 1.2em;
}
.draft-indicator-text {
flex: 1;
color: #001858;
font-size: 0.9em;
}
.draft-indicator-button {
padding: 5px 12px;
border: 1px solid #001858;
background-color: #fef6e4;
color: #001858;
border-radius: 3px;
cursor: pointer;
font-family: monospace;
font-size: 0.85em;
}
.draft-indicator-button:hover {
background-color: #001858;
color: #fef6e4;
}
.draft-indicator-button.restore {
background-color: #8bd3dd;
border-color: #001858;
}
.draft-indicator-button.restore:hover {
background-color: #001858;
color: #8bd3dd;
}
@keyframes slideInFromTop {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.spinner { .spinner {
border-color: #444; border-color: #444;
@ -724,6 +788,37 @@ input.error, textarea.error, select.error {
.char-counter { .char-counter {
color: #fef6e4; color: #fef6e4;
} }
.draft-indicator {
background-color: #2a2a2a;
border-color: #fef6e4;
}
.draft-indicator-text {
color: #fef6e4;
}
.draft-indicator-button {
background-color: #1a1a1a;
color: #fef6e4;
border-color: #fef6e4;
}
.draft-indicator-button:hover {
background-color: #fef6e4;
color: #1a1a1a;
}
.draft-indicator-button.restore {
background-color: #8bd3dd;
color: #001858;
border-color: #8bd3dd;
}
.draft-indicator-button.restore:hover {
background-color: #fef6e4;
color: #001858;
}
} }
@media (max-width: 600px) { @media (max-width: 600px) {

View File

@ -851,6 +851,61 @@
} }
}); });
// Draft auto-save functionality
const draftKey = 'draft_chat_{{.Board.ID}}';
let draftSaveTimeout;
// Load existing draft on page load
const existingDraft = loadDraft(draftKey);
const draftTimestamp = getDraftTimestamp(draftKey);
if (existingDraft && draftTimestamp) {
// Check if draft is less than 7 days old
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
if (draftTimestamp > sevenDaysAgo) {
const indicator = showDraftIndicator(
existingDraft,
draftTimestamp,
(content) => {
chatInput.value = content;
chatInput.focus();
},
() => {
clearDraft(draftKey);
}
);
// Insert indicator before chat input
const chatInputContainer = document.querySelector('.chat-input');
chatInputContainer.parentNode.insertBefore(indicator, chatInputContainer);
} else {
// Draft is too old, clear it
clearDraft(draftKey);
}
}
// Save draft on input (debounced)
chatInput.addEventListener('input', () => {
clearTimeout(draftSaveTimeout);
draftSaveTimeout = setTimeout(() => {
const content = chatInput.value.trim();
if (content) {
saveDraft(draftKey, content);
} else {
clearDraft(draftKey);
}
}, 2000); // Save after 2 seconds of inactivity
});
// Clear draft after successful message send
const originalSendMessage = window.sendMessage;
window.sendMessage = function() {
const input = document.getElementById('chat-input-text');
const content = input.value.trim();
if (content !== '') {
clearDraft(draftKey);
}
originalSendMessage();
};
// Initial check for scroll position // Initial check for scroll position
checkScrollPosition(); checkScrollPosition();
}; };