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
parent
309e516480
commit
e76049a353
|
|
@ -59,7 +59,6 @@ function enableEnterToSubmit(input, form) {
|
|||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -687,6 +687,70 @@ input.error, textarea.error, select.error {
|
|||
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) {
|
||||
.spinner {
|
||||
border-color: #444;
|
||||
|
|
@ -724,6 +788,37 @@ input.error, textarea.error, select.error {
|
|||
.char-counter {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
checkScrollPosition();
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue