Update et upgrade by codex
This commit is contained in:
parent
e3debb3f71
commit
4f4252686a
@ -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;
|
||||
|
||||
306
assets/js/app.js
306
assets/js/app.js
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user