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);
}
button.danger {
button.danger,
.btn.danger {
background: var(--accent-color);
}
button.danger:hover {
button.danger:hover,
.btn.danger:hover {
background: #c0392b;
}
button.success {
button.success,
.btn.success {
background: var(--success-color);
}
button.success:hover {
button.success:hover,
.btn.success:hover {
background: #229954;
}
@ -1176,6 +1180,36 @@ select:focus-visible {
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 {
display: flex;
flex-direction: column;

View File

@ -8,6 +8,9 @@ class ConceptionAssistant {
this.tocTimer = null;
this.isPreviewMode = false;
this.originalContent = '';
this.localBackupKey = 'design-journal-autosave';
this.localBackupTimer = null;
this.localStorageSupported = this.detectLocalStorageSupport();
this.init();
}
@ -15,6 +18,7 @@ class ConceptionAssistant {
this.setupEditor();
this.setupEventListeners();
await this.loadJournalList();
this.checkForLocalBackup();
this.generateTOC();
this.updateStatistics();
}
@ -26,6 +30,7 @@ class ConceptionAssistant {
this.editor.addEventListener('input', () => {
this.debounceTOC();
this.updateStatistics();
this.scheduleLocalBackup();
});
// Keyboard shortcut handling
@ -171,6 +176,134 @@ class ConceptionAssistant {
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() {
if (this.undoStack.length > 1) {
const currentContent = this.undoStack.pop();
@ -183,6 +316,7 @@ class ConceptionAssistant {
this.generateTOC();
this.showNotification('Undo completed', 'success');
this.saveLocalBackup(previousContent);
} else {
this.showNotification('Nothing to undo', 'warning');
}
@ -199,6 +333,7 @@ class ConceptionAssistant {
this.generateTOC();
this.showNotification('Redo completed', 'success');
this.saveLocalBackup(nextContent);
} else {
this.showNotification('Nothing to redo', 'warning');
}
@ -241,6 +376,8 @@ class ConceptionAssistant {
this.currentJournalId = result.data.id;
}
this.saveLocalBackup(content);
statusEl.textContent = 'Saved';
this.showNotification('Journal saved successfully', 'success');
setTimeout(() => statusEl.textContent = '', 3000);
@ -282,6 +419,8 @@ class ConceptionAssistant {
this.currentJournalId = id;
this.editor.innerText = journal.markdownContent;
this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(journal.markdownContent);
// Reset history for the new journal
this.undoStack = [journal.markdownContent];
@ -304,46 +443,12 @@ class ConceptionAssistant {
const result = await response.json();
if (result.success) {
const journalList = result.data.map(journal => {
// 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>
`;
this.renderJournalSelector(result.data);
// Show modal with animation
const modal = document.getElementById('journal-modal');
modal.style.display = 'flex';
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) {
console.error('Error:', error);
@ -359,11 +464,133 @@ class ConceptionAssistant {
}, 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() {
this.currentJournalId = null;
this.editor.innerText = '';
this.generateTOC();
this.updateStatistics();
this.clearFeedback();
this.saveLocalBackup('');
// Reset history for new journal
this.undoStack = [''];
@ -391,6 +618,7 @@ class ConceptionAssistant {
// Update statistics
this.updateStatistics();
this.saveLocalBackup('');
// Show notification
this.showNotification('New blank document created', 'success');
@ -750,6 +978,8 @@ class ConceptionAssistant {
const importedContent = e.target.result;
this.editor.innerText = importedContent;
this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(importedContent);
this.currentJournalId = null; // New journal
// Reset history for imported file
@ -1162,6 +1392,8 @@ class ConceptionAssistant {
if (data.markdown && data.markdown !== this.editor.innerText) {
this.editor.innerText = data.markdown;
this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(data.markdown);
}
// Scroll to bottom of feedback to see new iteration
@ -1179,6 +1411,8 @@ class ConceptionAssistant {
// Ensure final content is in editor
this.editor.innerText = data.finalMarkdown;
this.generateTOC();
this.updateStatistics();
this.saveLocalBackup(data.finalMarkdown);
// Save final state
this.saveState(true);
}
@ -1649,6 +1883,8 @@ function initializeTemplateForm() {
// Load template into editor
app.editor.innerText = result.data.content;
app.generateTOC();
app.updateStatistics();
app.saveLocalBackup(result.data.content);
app.currentJournalId = null; // New journal
// Reset history for new template

View File

@ -53,23 +53,30 @@ function createMd(markdownContent = "# Title\nContent...") {
const mdPath = path.join(dataDir, `${uuid}.md`);
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' });
let map = {};
if (fs.existsSync(mapPath)) {
map = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
}
let id = Object.keys(map).at(-1);
if (id == undefined){
map[0] = uuid;
} else {
id = +id+1
map[id] = uuid;
}
const existingIds = Object.keys(map)
.map((key) => Number.parseInt(key, 10))
.filter((value) => Number.isInteger(value) && value >= 0);
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');
return { id, uuid, path: mdPath, markdownContent};
return { id, uuid, path: mdPath, markdownContent };
}
function readMd(id = undefined) {