Update et upgrade by codex

This commit is contained in:
Augustin ROUX 2025-10-24 12:48:48 +02:00
parent e3debb3f71
commit 4f4252686a
3 changed files with 327 additions and 50 deletions

View File

@ -485,20 +485,24 @@ button.secondary:hover {
border-color: var(--secondary-color); border-color: var(--secondary-color);
} }
button.danger { button.danger,
.btn.danger {
background: var(--accent-color); background: var(--accent-color);
} }
button.danger:hover { button.danger:hover,
.btn.danger:hover {
background: #c0392b; background: #c0392b;
} }
button.success { button.success,
.btn.success {
background: var(--success-color); background: var(--success-color);
} }
button.success:hover { button.success:hover,
.btn.success:hover {
background: #229954; background: #229954;
} }
@ -1176,6 +1180,36 @@ select:focus-visible {
box-shadow: var(--shadow-hover); box-shadow: var(--shadow-hover);
} }
.journal-item.active {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(41, 128, 185, 0.15);
}
.journal-item-actions {
display: flex;
align-items: stretch;
gap: 0.5rem;
}
.journal-item-actions .journal-item {
flex: 1;
}
.journal-item-actions .journal-delete {
flex-shrink: 0;
padding: 0.6rem 0.9rem;
text-transform: none;
}
.journal-list.empty {
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: center;
text-align: center;
color: var(--text-light);
}
.journal-preview { .journal-preview {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1317,4 +1351,4 @@ select:focus-visible {
.mt-1 { margin-top: 0.5rem; } .mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; } .mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; } .mt-3 { margin-top: 1.5rem; }

View File

@ -8,6 +8,9 @@ class ConceptionAssistant {
this.tocTimer = null; this.tocTimer = null;
this.isPreviewMode = false; this.isPreviewMode = false;
this.originalContent = ''; this.originalContent = '';
this.localBackupKey = 'design-journal-autosave';
this.localBackupTimer = null;
this.localStorageSupported = this.detectLocalStorageSupport();
this.init(); this.init();
} }
@ -15,6 +18,7 @@ class ConceptionAssistant {
this.setupEditor(); this.setupEditor();
this.setupEventListeners(); this.setupEventListeners();
await this.loadJournalList(); await this.loadJournalList();
this.checkForLocalBackup();
this.generateTOC(); this.generateTOC();
this.updateStatistics(); this.updateStatistics();
} }
@ -26,6 +30,7 @@ class ConceptionAssistant {
this.editor.addEventListener('input', () => { this.editor.addEventListener('input', () => {
this.debounceTOC(); this.debounceTOC();
this.updateStatistics(); this.updateStatistics();
this.scheduleLocalBackup();
}); });
// Keyboard shortcut handling // Keyboard shortcut handling
@ -171,6 +176,134 @@ class ConceptionAssistant {
this.redoStack = []; this.redoStack = [];
} }
detectLocalStorageSupport() {
if (typeof window === 'undefined') {
return false;
}
try {
const testKey = '__design_journal_test__';
window.localStorage.setItem(testKey, '1');
window.localStorage.removeItem(testKey);
return true;
} catch (error) {
console.warn('Local storage unavailable, disabling local backups.', error);
return false;
}
}
scheduleLocalBackup() {
if (!this.localStorageSupported) return;
if (this.localBackupTimer) {
clearTimeout(this.localBackupTimer);
}
this.localBackupTimer = setTimeout(() => {
this.saveLocalBackup();
}, 1500);
}
saveLocalBackup(contentOverride = null) {
if (!this.localStorageSupported || typeof window === 'undefined') return;
try {
const content = typeof contentOverride === 'string'
? contentOverride
: (this.editor ? this.editor.innerText : '');
const normalizedContent = content || '';
if (!normalizedContent.trim()) {
window.localStorage.removeItem(this.localBackupKey);
return;
}
const backupPayload = {
content: normalizedContent,
timestamp: new Date().toISOString(),
journalId: this.currentJournalId
};
window.localStorage.setItem(this.localBackupKey, JSON.stringify(backupPayload));
} catch (error) {
console.warn('Unable to persist local backup.', error);
}
}
discardLocalBackup() {
if (!this.localStorageSupported || typeof window === 'undefined') return;
try {
window.localStorage.removeItem(this.localBackupKey);
} catch (error) {
console.warn('Unable to discard local backup.', error);
}
}
checkForLocalBackup() {
if (!this.localStorageSupported || typeof window === 'undefined') return;
let rawBackup;
try {
rawBackup = window.localStorage.getItem(this.localBackupKey);
} catch (error) {
console.warn('Unable to access local backup.', error);
return;
}
if (!rawBackup) return;
let backup;
try {
backup = JSON.parse(rawBackup);
} catch (error) {
console.warn('Invalid local backup payload, clearing it.', error);
this.discardLocalBackup();
return;
}
if (!backup || typeof backup.content !== 'string' || !backup.content.trim()) {
this.discardLocalBackup();
return;
}
const currentContent = this.editor ? this.editor.innerText : '';
if (currentContent.trim() === backup.content.trim()) {
return;
}
const savedAt = backup.timestamp ? new Date(backup.timestamp) : null;
const formattedDate = savedAt && !Number.isNaN(savedAt.getTime())
? savedAt.toLocaleString()
: 'a previous session';
const shouldRestore = window.confirm(
`A local draft saved on ${formattedDate} was found. Do you want to restore it?`
);
if (!shouldRestore) {
this.discardLocalBackup();
return;
}
this.editor.innerText = backup.content;
this.currentJournalId = backup.journalId ?? this.currentJournalId;
this.undoStack = [backup.content];
this.redoStack = [];
this.generateTOC();
this.updateStatistics();
this.saveState(true);
this.saveLocalBackup(backup.content);
const statusEl = document.getElementById('save-status');
if (statusEl) {
statusEl.textContent = 'Local draft restored';
setTimeout(() => statusEl.textContent = '', 4000);
}
this.showNotification('Local draft restored', 'success');
}
undo() { undo() {
if (this.undoStack.length > 1) { if (this.undoStack.length > 1) {
const currentContent = this.undoStack.pop(); const currentContent = this.undoStack.pop();
@ -183,6 +316,7 @@ class ConceptionAssistant {
this.generateTOC(); this.generateTOC();
this.showNotification('Undo completed', 'success'); this.showNotification('Undo completed', 'success');
this.saveLocalBackup(previousContent);
} else { } else {
this.showNotification('Nothing to undo', 'warning'); this.showNotification('Nothing to undo', 'warning');
} }
@ -199,6 +333,7 @@ class ConceptionAssistant {
this.generateTOC(); this.generateTOC();
this.showNotification('Redo completed', 'success'); this.showNotification('Redo completed', 'success');
this.saveLocalBackup(nextContent);
} else { } else {
this.showNotification('Nothing to redo', 'warning'); this.showNotification('Nothing to redo', 'warning');
} }
@ -241,6 +376,8 @@ class ConceptionAssistant {
this.currentJournalId = result.data.id; this.currentJournalId = result.data.id;
} }
this.saveLocalBackup(content);
statusEl.textContent = 'Saved'; statusEl.textContent = 'Saved';
this.showNotification('Journal saved successfully', 'success'); this.showNotification('Journal saved successfully', 'success');
setTimeout(() => statusEl.textContent = '', 3000); setTimeout(() => statusEl.textContent = '', 3000);
@ -282,6 +419,8 @@ class ConceptionAssistant {
this.currentJournalId = id; this.currentJournalId = id;
this.editor.innerText = journal.markdownContent; this.editor.innerText = journal.markdownContent;
this.generateTOC(); this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(journal.markdownContent);
// Reset history for the new journal // Reset history for the new journal
this.undoStack = [journal.markdownContent]; this.undoStack = [journal.markdownContent];
@ -304,46 +443,12 @@ class ConceptionAssistant {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
const journalList = result.data.map(journal => { this.renderJournalSelector(result.data);
// Extract first lines for preview
const lines = journal.markdownContent.split('\n');
const firstLines = lines.slice(0, 3).join('\n');
const preview = firstLines.length > 150 ? firstLines.substring(0, 150) + '...' : firstLines;
return `
<div class="journal-item-container">
<button class="journal-item btn secondary" data-id="${journal.id}">
<div class="journal-preview">
<strong>Journal ${journal.id}</strong>
<div>${preview}</div>
</div>
</button>
</div>
`;
}).join('');
const modalBody = document.getElementById('journal-modal-body');
modalBody.innerHTML = `
<div class="journal-list">
<button class="btn success mb-2" onclick="app.createNewJournal(); app.closeModal();" style="width: 100%;">
+ New Journal
</button>
${journalList}
</div>
`;
// Show modal with animation // Show modal with animation
const modal = document.getElementById('journal-modal'); const modal = document.getElementById('journal-modal');
modal.style.display = 'flex'; modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10); setTimeout(() => modal.classList.add('show'), 10);
// Add event listeners
document.querySelectorAll('.journal-item').forEach(btn => {
btn.addEventListener('click', () => {
this.loadJournal(btn.dataset.id);
this.closeModal();
});
});
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
@ -359,11 +464,133 @@ class ConceptionAssistant {
}, 300); }, 300);
} }
renderJournalSelector(journals = []) {
const modalBody = document.getElementById('journal-modal-body');
if (!modalBody) return;
if (!Array.isArray(journals) || journals.length === 0) {
modalBody.innerHTML = `
<div class="journal-list empty">
<p>No journals saved yet.</p>
<button class="btn success" onclick="app.createNewJournal(); app.closeModal();" style="width: 100%;">
+ New Journal
</button>
</div>
`;
return;
}
const journalList = journals.map(journal => {
const lines = journal.markdownContent.split('\n');
const firstLines = lines.slice(0, 3).join('\n');
const preview = firstLines.length > 150 ? firstLines.substring(0, 150) + '...' : firstLines;
const isCurrent = String(this.currentJournalId) === String(journal.id);
return `
<div class="journal-item-container">
<div class="journal-item-actions">
<button class="journal-item btn secondary${isCurrent ? ' active' : ''}" data-id="${journal.id}">
<div class="journal-preview">
<strong>Journal ${journal.id}${isCurrent ? ' (current)' : ''}</strong>
<div>${preview}</div>
</div>
</button>
<button class="btn danger journal-delete" data-id="${journal.id}" title="Delete journal ${journal.id}">
Delete
</button>
</div>
</div>
`;
}).join('');
modalBody.innerHTML = `
<div class="journal-list">
<button class="btn success mb-2" onclick="app.createNewJournal(); app.closeModal();" style="width: 100%;">
+ New Journal
</button>
${journalList}
</div>
`;
// Add load listeners
modalBody.querySelectorAll('.journal-item').forEach(btn => {
btn.addEventListener('click', () => {
this.loadJournal(btn.dataset.id);
this.closeModal();
});
});
// Add delete listeners
modalBody.querySelectorAll('.journal-delete').forEach(btn => {
btn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
this.deleteJournal(btn.dataset.id, btn);
});
});
}
async deleteJournal(id, buttonEl) {
if (!id) return;
const confirmation = window.confirm(`Are you sure you want to delete journal ${id}? This action cannot be undone.`);
if (!confirmation) return;
if (buttonEl) {
buttonEl.disabled = true;
buttonEl.textContent = 'Deleting...';
}
try {
const response = await fetch(`/api/journals/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Deletion failed');
}
this.showNotification(`Journal ${id} deleted`, 'success');
// Refresh list of journals
const refreshResponse = await fetch('/api/journals');
const refreshResult = await refreshResponse.json();
if (refreshResult.success) {
const remaining = refreshResult.data;
// If the current journal was deleted, load another available journal or clear editor
if (String(this.currentJournalId) === String(id)) {
if (remaining.length > 0) {
const lastJournal = remaining[remaining.length - 1];
await this.loadJournal(lastJournal.id);
} else {
this.createNewJournal();
}
}
this.renderJournalSelector(remaining);
}
} catch (error) {
console.error('Error deleting journal:', error);
this.showNotification(`Error deleting journal: ${error.message}`, 'error');
} finally {
if (buttonEl) {
buttonEl.disabled = false;
buttonEl.textContent = 'Delete';
}
}
}
createNewJournal() { createNewJournal() {
this.currentJournalId = null; this.currentJournalId = null;
this.editor.innerText = ''; this.editor.innerText = '';
this.generateTOC(); this.generateTOC();
this.updateStatistics();
this.clearFeedback(); this.clearFeedback();
this.saveLocalBackup('');
// Reset history for new journal // Reset history for new journal
this.undoStack = ['']; this.undoStack = [''];
@ -391,6 +618,7 @@ class ConceptionAssistant {
// Update statistics // Update statistics
this.updateStatistics(); this.updateStatistics();
this.saveLocalBackup('');
// Show notification // Show notification
this.showNotification('New blank document created', 'success'); this.showNotification('New blank document created', 'success');
@ -750,6 +978,8 @@ class ConceptionAssistant {
const importedContent = e.target.result; const importedContent = e.target.result;
this.editor.innerText = importedContent; this.editor.innerText = importedContent;
this.generateTOC(); this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(importedContent);
this.currentJournalId = null; // New journal this.currentJournalId = null; // New journal
// Reset history for imported file // Reset history for imported file
@ -1162,6 +1392,8 @@ class ConceptionAssistant {
if (data.markdown && data.markdown !== this.editor.innerText) { if (data.markdown && data.markdown !== this.editor.innerText) {
this.editor.innerText = data.markdown; this.editor.innerText = data.markdown;
this.generateTOC(); this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(data.markdown);
} }
// Scroll to bottom of feedback to see new iteration // Scroll to bottom of feedback to see new iteration
@ -1179,6 +1411,8 @@ class ConceptionAssistant {
// Ensure final content is in editor // Ensure final content is in editor
this.editor.innerText = data.finalMarkdown; this.editor.innerText = data.finalMarkdown;
this.generateTOC(); this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(data.finalMarkdown);
// Save final state // Save final state
this.saveState(true); this.saveState(true);
} }
@ -1649,6 +1883,8 @@ function initializeTemplateForm() {
// Load template into editor // Load template into editor
app.editor.innerText = result.data.content; app.editor.innerText = result.data.content;
app.generateTOC(); app.generateTOC();
app.updateStatistics();
app.saveLocalBackup(result.data.content);
app.currentJournalId = null; // New journal app.currentJournalId = null; // New journal
// Reset history for new template // Reset history for new template
@ -1674,4 +1910,4 @@ function initializeTemplateForm() {
} }
// Ensure togglePanel is globally accessible // Ensure togglePanel is globally accessible
window.togglePanel = togglePanel; window.togglePanel = togglePanel;

View File

@ -53,23 +53,30 @@ function createMd(markdownContent = "# Title\nContent...") {
const mdPath = path.join(dataDir, `${uuid}.md`); const mdPath = path.join(dataDir, `${uuid}.md`);
const mapPath = path.join(dataDir, 'uuid_map.json'); const mapPath = path.join(dataDir, 'uuid_map.json');
// Guarantee storage directory exists (defensive in case server init didn't run)
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
logger.info('STORAGE', 'Created data directory on demand', { path: dataDir });
}
fs.writeFileSync(mdPath, markdownContent, { encoding: 'utf8', flag: 'w' }); fs.writeFileSync(mdPath, markdownContent, { encoding: 'utf8', flag: 'w' });
let map = {}; let map = {};
if (fs.existsSync(mapPath)) { if (fs.existsSync(mapPath)) {
map = JSON.parse(fs.readFileSync(mapPath, 'utf8')); map = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
} }
let id = Object.keys(map).at(-1);
if (id == undefined){ const existingIds = Object.keys(map)
map[0] = uuid; .map((key) => Number.parseInt(key, 10))
} else { .filter((value) => Number.isInteger(value) && value >= 0);
id = +id+1
map[id] = uuid; const nextIdNumber = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 0;
} const id = String(nextIdNumber);
map[id] = uuid;
fs.writeFileSync(mapPath, JSON.stringify(map, null, 2), 'utf8'); fs.writeFileSync(mapPath, JSON.stringify(map, null, 2), 'utf8');
return { id, uuid, path: mdPath, markdownContent}; return { id, uuid, path: mdPath, markdownContent };
} }
function readMd(id = undefined) { function readMd(id = undefined) {
@ -259,4 +266,4 @@ router.delete('/journals/:id', (req, res) => {
// Integrate export routes // Integrate export routes
router.use('/export', exportRouter); router.use('/export', exportRouter);
module.exports = router; module.exports = router;