1914 lines
61 KiB
JavaScript
1914 lines
61 KiB
JavaScript
// Main application
|
|
class ConceptionAssistant {
|
|
constructor() {
|
|
this.currentJournalId = null;
|
|
this.editor = null;
|
|
this.undoStack = [];
|
|
this.redoStack = [];
|
|
this.tocTimer = null;
|
|
this.isPreviewMode = false;
|
|
this.originalContent = '';
|
|
this.localBackupKey = 'design-journal-autosave';
|
|
this.localBackupTimer = null;
|
|
this.localStorageSupported = this.detectLocalStorageSupport();
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
this.setupEditor();
|
|
this.setupEventListeners();
|
|
await this.loadJournalList();
|
|
this.checkForLocalBackup();
|
|
this.generateTOC();
|
|
this.updateStatistics();
|
|
}
|
|
|
|
setupEditor() {
|
|
this.editor = document.getElementById('journal-editor');
|
|
|
|
// Generate TOC with debounce to avoid interrupting typing
|
|
this.editor.addEventListener('input', () => {
|
|
this.debounceTOC();
|
|
this.updateStatistics();
|
|
this.scheduleLocalBackup();
|
|
});
|
|
|
|
// Keyboard shortcut handling
|
|
this.editor.addEventListener('keydown', (e) => {
|
|
// Ctrl+S to save
|
|
if (e.ctrlKey && e.key === 's') {
|
|
e.preventDefault();
|
|
this.saveJournal();
|
|
}
|
|
|
|
// Ctrl+Z to undo
|
|
if (e.ctrlKey && e.key === 'z' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
this.undo();
|
|
}
|
|
|
|
// Ctrl+Y or Ctrl+Shift+Z to redo
|
|
if (e.ctrlKey && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) {
|
|
e.preventDefault();
|
|
this.redo();
|
|
}
|
|
|
|
// Tab for indentation
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
document.execCommand('insertText', false, ' ');
|
|
}
|
|
});
|
|
|
|
// Save state for undo/redo before important modifications
|
|
this.editor.addEventListener('input', () => {
|
|
this.saveState();
|
|
});
|
|
}
|
|
|
|
showEditorPlaceholder() {
|
|
// Placeholder is now managed via CSS ::before
|
|
// This function can be removed or left empty for compatibility
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Journal control buttons
|
|
document.getElementById('save-journal')?.addEventListener('click', () => this.saveJournal());
|
|
document.getElementById('load-journal')?.addEventListener('click', () => this.showJournalSelector());
|
|
document.getElementById('preview-toggle')?.addEventListener('click', async () => await this.togglePreview());
|
|
document.getElementById('new-blank-document')?.addEventListener('click', () => this.createBlankDocument());
|
|
|
|
// Table of contents
|
|
document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC());
|
|
|
|
// Export/Import
|
|
document.getElementById('export-md')?.addEventListener('click', () => this.exportMarkdown());
|
|
document.getElementById('export-web')?.addEventListener('click', () => this.exportWeb());
|
|
document.getElementById('import-md')?.addEventListener('change', (e) => this.importMarkdown(e));
|
|
|
|
// Theme
|
|
document.getElementById('theme-toggle')?.addEventListener('click', () => this.toggleTheme());
|
|
|
|
// AI Assistant
|
|
document.getElementById('activate-rephrase')?.addEventListener('click', () => this.handleAI('rephrase'));
|
|
document.getElementById('check-inconsistencies')?.addEventListener('click', () => this.handleAI('inconsistencies'));
|
|
document.getElementById('check-duplications')?.addEventListener('click', () => this.handleAI('duplications'));
|
|
document.getElementById('give-advice')?.addEventListener('click', () => this.handleAI('advice'));
|
|
document.getElementById('liberty-mode')?.addEventListener('click', () => this.handleAI('liberty'));
|
|
|
|
// Present mode
|
|
document.getElementById('present-mode')?.addEventListener('click', () => this.openPresentMode());
|
|
|
|
// Shortcuts help
|
|
document.getElementById('show-shortcuts')?.addEventListener('click', () => this.showShortcutsModal());
|
|
|
|
// Loading modal
|
|
document.getElementById('close-journal-modal')?.addEventListener('click', () => this.closeModal());
|
|
|
|
// Close modal by clicking on overlay
|
|
document.getElementById('journal-modal')?.addEventListener('click', (e) => {
|
|
if (e.target.id === 'journal-modal') {
|
|
this.closeModal();
|
|
}
|
|
});
|
|
|
|
// Scroll to top button
|
|
document.getElementById('scroll-to-top')?.addEventListener('click', () => this.scrollToTop());
|
|
|
|
// Manage scroll to top button display
|
|
this.setupScrollToTop();
|
|
}
|
|
|
|
setupScrollToTop() {
|
|
const scrollButton = document.getElementById('scroll-to-top');
|
|
if (!scrollButton) return;
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (window.scrollY > 300) {
|
|
scrollButton.classList.add('visible');
|
|
} else {
|
|
scrollButton.classList.remove('visible');
|
|
}
|
|
});
|
|
}
|
|
|
|
scrollToTop() {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
this.showNotification('Back to top', 'success');
|
|
}
|
|
|
|
saveState(immediate = false) {
|
|
// If immediate is true, save immediately without debounce
|
|
if (immediate) {
|
|
this.performSaveState();
|
|
return;
|
|
}
|
|
|
|
// Avoid saving too often (debounce)
|
|
if (this.saveStateTimer) {
|
|
clearTimeout(this.saveStateTimer);
|
|
}
|
|
|
|
this.saveStateTimer = setTimeout(() => {
|
|
this.performSaveState();
|
|
}, 1000); // Save after 1 second of inactivity
|
|
}
|
|
|
|
performSaveState() {
|
|
const currentContent = this.editor.innerText;
|
|
|
|
// Don't save if content hasn't changed
|
|
if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === currentContent) {
|
|
return;
|
|
}
|
|
|
|
this.undoStack.push(currentContent);
|
|
|
|
// Limit undo stack to 50 elements
|
|
if (this.undoStack.length > 50) {
|
|
this.undoStack.shift();
|
|
}
|
|
|
|
// Clear redo stack because we've done a new action
|
|
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();
|
|
this.redoStack.push(currentContent);
|
|
|
|
const previousContent = this.undoStack[this.undoStack.length - 1];
|
|
this.editor.innerText = previousContent;
|
|
|
|
// Regenerate table of contents
|
|
this.generateTOC();
|
|
|
|
this.showNotification('Undo completed', 'success');
|
|
this.saveLocalBackup(previousContent);
|
|
} else {
|
|
this.showNotification('Nothing to undo', 'warning');
|
|
}
|
|
}
|
|
|
|
redo() {
|
|
if (this.redoStack.length > 0) {
|
|
const nextContent = this.redoStack.pop();
|
|
this.undoStack.push(nextContent);
|
|
|
|
this.editor.innerText = nextContent;
|
|
|
|
// Regenerate table of contents
|
|
this.generateTOC();
|
|
|
|
this.showNotification('Redo completed', 'success');
|
|
this.saveLocalBackup(nextContent);
|
|
} else {
|
|
this.showNotification('Nothing to redo', 'warning');
|
|
}
|
|
}
|
|
|
|
|
|
async saveJournal() {
|
|
const content = this.editor.innerText;
|
|
if (!content) return;
|
|
|
|
const statusEl = document.getElementById('save-status');
|
|
const saveBtn = document.getElementById('save-journal');
|
|
|
|
saveBtn.classList.add('loading');
|
|
statusEl.textContent = 'Saving...';
|
|
|
|
try {
|
|
let response;
|
|
|
|
if (this.currentJournalId) {
|
|
// Update
|
|
response = await fetch(`/api/journals/${this.currentJournalId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content })
|
|
});
|
|
} else {
|
|
// Create
|
|
response = await fetch('/api/journals', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content })
|
|
});
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
if (!this.currentJournalId) {
|
|
this.currentJournalId = result.data.id;
|
|
}
|
|
|
|
this.saveLocalBackup(content);
|
|
|
|
statusEl.textContent = 'Saved';
|
|
this.showNotification('Journal saved successfully', 'success');
|
|
setTimeout(() => statusEl.textContent = '', 3000);
|
|
} else {
|
|
throw new Error(result.error || 'Save error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
statusEl.textContent = 'Error';
|
|
this.showNotification('Error while saving: ' + error.message, 'error');
|
|
setTimeout(() => statusEl.textContent = '', 3000);
|
|
} finally {
|
|
saveBtn.classList.remove('loading');
|
|
}
|
|
}
|
|
|
|
async loadJournalList() {
|
|
try {
|
|
const response = await fetch('/api/journals');
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.data.length > 0) {
|
|
// Load the last journal automatically
|
|
const lastJournal = result.data[result.data.length - 1];
|
|
await this.loadJournal(lastJournal.id);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading list:', error);
|
|
}
|
|
}
|
|
|
|
async loadJournal(id) {
|
|
try {
|
|
const response = await fetch(`/api/journals/${id}`);
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.data.length > 0) {
|
|
const journal = result.data[0];
|
|
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];
|
|
this.redoStack = [];
|
|
|
|
// Ensure editor is in edit mode
|
|
this.ensureEditMode();
|
|
|
|
this.showNotification('Journal loaded', 'success');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading journal:', error);
|
|
this.showNotification('Error while loading', 'error');
|
|
}
|
|
}
|
|
|
|
async showJournalSelector() {
|
|
try {
|
|
const response = await fetch('/api/journals');
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.renderJournalSelector(result.data);
|
|
|
|
// Show modal with animation
|
|
const modal = document.getElementById('journal-modal');
|
|
modal.style.display = 'flex';
|
|
setTimeout(() => modal.classList.add('show'), 10);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
this.showNotification('Error loading list', 'error');
|
|
}
|
|
}
|
|
|
|
closeModal() {
|
|
const modal = document.getElementById('journal-modal');
|
|
modal.classList.remove('show');
|
|
setTimeout(() => {
|
|
modal.style.display = 'none';
|
|
}, 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 = [''];
|
|
this.redoStack = [];
|
|
|
|
// Ensure editor is in edit mode
|
|
this.ensureEditMode();
|
|
|
|
this.showNotification('New journal created', 'success');
|
|
}
|
|
|
|
createBlankDocument() {
|
|
// Clear the editor completely
|
|
this.editor.innerText = '';
|
|
|
|
// Reset currentJournalId to null
|
|
this.currentJournalId = null;
|
|
|
|
// Reset undo/redo stacks
|
|
this.undoStack = [''];
|
|
this.redoStack = [];
|
|
|
|
// Generate TOC
|
|
this.generateTOC();
|
|
|
|
// Update statistics
|
|
this.updateStatistics();
|
|
this.saveLocalBackup('');
|
|
|
|
// Show notification
|
|
this.showNotification('New blank document created', 'success');
|
|
|
|
// Close all panels
|
|
closeAllPanels();
|
|
|
|
// Ensure editor is in edit mode
|
|
this.ensureEditMode();
|
|
}
|
|
|
|
debounceTOC() {
|
|
if (this.tocTimer) {
|
|
clearTimeout(this.tocTimer);
|
|
}
|
|
this.tocTimer = setTimeout(() => {
|
|
this.generateTOC();
|
|
}, 500); // Wait 500ms after last keystroke
|
|
}
|
|
|
|
saveSelection() {
|
|
const selection = window.getSelection();
|
|
if (selection.rangeCount > 0) {
|
|
return selection.getRangeAt(0).cloneRange();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
restoreSelection(range) {
|
|
if (range) {
|
|
const selection = window.getSelection();
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
}
|
|
|
|
cleanMarkdownFromTitle(title) {
|
|
// Remove all markdown formatting markers from title
|
|
return title
|
|
.replace(/\*\*/g, '') // Remove bold **
|
|
.replace(/\*/g, '') // Remove italic *
|
|
.replace(/__/g, '') // Remove bold __
|
|
.replace(/_/g, '') // Remove italic _
|
|
.replace(/`/g, '') // Remove code `
|
|
.replace(/~~/g, '') // Remove strikethrough ~~
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links, keep text
|
|
.trim();
|
|
}
|
|
|
|
generateTOC() {
|
|
// Save cursor position
|
|
const savedRange = this.saveSelection();
|
|
|
|
const content = this.editor.innerText;
|
|
const lines = content.split('\n');
|
|
const toc = [];
|
|
|
|
// Remove existing anchors
|
|
const existingAnchors = this.editor.querySelectorAll('.heading-anchor');
|
|
existingAnchors.forEach(anchor => anchor.remove());
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (line.startsWith('#')) {
|
|
const level = line.match(/^#+/)[0].length;
|
|
const title = line.replace(/^#+\s*/, '');
|
|
const id = 'heading-' + i;
|
|
|
|
toc.push({ level, title, id, lineIndex: i });
|
|
}
|
|
}
|
|
|
|
let tocHtml = '';
|
|
if (toc.length > 0) {
|
|
// Nouvelle génération de TOC avec indentation correcte
|
|
let previousLevel = 0;
|
|
|
|
for (let i = 0; i < toc.length; i++) {
|
|
const item = toc[i];
|
|
|
|
// Ouvrir les listes nécessaires
|
|
if (item.level > previousLevel) {
|
|
for (let j = previousLevel; j < item.level; j++) {
|
|
tocHtml += '<ul>';
|
|
}
|
|
}
|
|
// Fermer les listes nécessaires
|
|
else if (item.level < previousLevel) {
|
|
for (let j = item.level; j < previousLevel; j++) {
|
|
tocHtml += '</ul></li>';
|
|
}
|
|
}
|
|
// Même niveau, fermer le li précédent
|
|
else if (i > 0) {
|
|
tocHtml += '</li>';
|
|
}
|
|
|
|
// Ajouter l'élément avec indentation CSS
|
|
const indent = (item.level - 1) * 1; // 1rem par niveau
|
|
const cleanTitle = this.cleanMarkdownFromTitle(item.title);
|
|
tocHtml += `<li style="margin-left: ${indent}rem;"><a href="#${item.id}" onclick="app.scrollToHeading('${item.title.replace(/'/g, "\\'")}'); return false;">${cleanTitle}</a>`;
|
|
|
|
previousLevel = item.level;
|
|
}
|
|
|
|
// Fermer toutes les listes restantes
|
|
for (let j = 0; j < previousLevel; j++) {
|
|
tocHtml += '</li></ul>';
|
|
}
|
|
} else {
|
|
tocHtml = '<div class="toc-placeholder"><p>Add headings (# ## ###) to your journal to generate the table of contents.</p></div>';
|
|
}
|
|
|
|
document.getElementById('toc-nav').innerHTML = tocHtml;
|
|
|
|
// Restore cursor position
|
|
this.restoreSelection(savedRange);
|
|
}
|
|
|
|
addHeadingAnchors(lines, toc) {
|
|
const editorLines = this.editor.innerHTML.split('<br>');
|
|
let newContent = '';
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const tocItem = toc.find(item => item.lineIndex === i);
|
|
|
|
if (tocItem) {
|
|
newContent += `<span class="heading-anchor" id="${tocItem.id}" style="position: absolute; margin-top: -100px; visibility: hidden;"></span>`;
|
|
}
|
|
|
|
newContent += line;
|
|
if (i < lines.length - 1) newContent += '<br>';
|
|
}
|
|
|
|
this.editor.innerHTML = newContent;
|
|
}
|
|
|
|
scrollToHeading(title) {
|
|
try {
|
|
const content = this.editor.innerText;
|
|
const lines = content.split('\n');
|
|
|
|
// Find the line index and calculate character position
|
|
let targetLineIndex = -1;
|
|
let charPosition = 0;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
|
|
targetLineIndex = i;
|
|
break;
|
|
}
|
|
// Count characters including newlines
|
|
charPosition += lines[i].length + 1; // +1 for \n
|
|
}
|
|
|
|
if (targetLineIndex === -1) {
|
|
this.showNotification('Section not found', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Create a range at the target position to get accurate coordinates
|
|
const selection = window.getSelection();
|
|
const range = document.createRange();
|
|
|
|
// Find the text node at the character position
|
|
const walker = document.createTreeWalker(
|
|
this.editor,
|
|
NodeFilter.SHOW_TEXT,
|
|
null
|
|
);
|
|
|
|
let currentChar = 0;
|
|
let targetNode = null;
|
|
let offsetInNode = 0;
|
|
|
|
while (walker.nextNode()) {
|
|
const node = walker.currentNode;
|
|
const nodeLength = node.textContent.length;
|
|
|
|
if (currentChar + nodeLength >= charPosition) {
|
|
targetNode = node;
|
|
offsetInNode = charPosition - currentChar;
|
|
break;
|
|
}
|
|
currentChar += nodeLength;
|
|
}
|
|
|
|
if (targetNode) {
|
|
// Set range at the target position
|
|
range.setStart(targetNode, Math.min(offsetInNode, targetNode.textContent.length));
|
|
range.setEnd(targetNode, Math.min(offsetInNode, targetNode.textContent.length));
|
|
|
|
// Get the bounding rectangle of the range
|
|
const rect = range.getBoundingClientRect();
|
|
|
|
// Scroll to the element - use the parent container (#design-journal)
|
|
const journalSection = document.getElementById('design-journal');
|
|
const sectionRect = journalSection.getBoundingClientRect();
|
|
const scrollOffset = rect.top - sectionRect.top + journalSection.scrollTop - 100;
|
|
|
|
journalSection.scrollTo({
|
|
top: Math.max(0, scrollOffset),
|
|
behavior: 'smooth'
|
|
});
|
|
|
|
// Temporarily highlight the heading
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
setTimeout(() => selection.removeAllRanges(), 1000);
|
|
}
|
|
|
|
this.showNotification(`Navigating to: ${title}`, 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Scroll error:', error);
|
|
this.showNotification('Error during navigation', 'error');
|
|
}
|
|
}
|
|
|
|
highlightHeading(title) {
|
|
// Function to temporarily highlight the found title
|
|
try {
|
|
const content = this.editor.innerText;
|
|
const lines = content.split('\n');
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) {
|
|
// Create a range to select the line
|
|
const selection = window.getSelection();
|
|
const range = document.createRange();
|
|
|
|
// Find the text node and position
|
|
const walker = document.createTreeWalker(
|
|
this.editor,
|
|
NodeFilter.SHOW_TEXT,
|
|
null,
|
|
false
|
|
);
|
|
|
|
let currentLine = 0;
|
|
let textNode = walker.nextNode();
|
|
|
|
while (textNode && currentLine < i) {
|
|
const nodeText = textNode.textContent;
|
|
const newLines = (nodeText.match(/\n/g) || []).length;
|
|
currentLine += newLines;
|
|
|
|
if (currentLine < i) {
|
|
textNode = walker.nextNode();
|
|
}
|
|
}
|
|
|
|
if (textNode) {
|
|
// Find the start of the line in this node
|
|
const nodeText = textNode.textContent;
|
|
const linesInNode = nodeText.split('\n');
|
|
const targetLineInNode = i - (currentLine - linesInNode.length + 1);
|
|
|
|
if (targetLineInNode >= 0 && targetLineInNode < linesInNode.length) {
|
|
let startPos = 0;
|
|
for (let j = 0; j < targetLineInNode; j++) {
|
|
startPos += linesInNode[j].length + 1;
|
|
}
|
|
|
|
const endPos = startPos + linesInNode[targetLineInNode].length;
|
|
|
|
range.setStart(textNode, startPos);
|
|
range.setEnd(textNode, endPos);
|
|
|
|
// Select temporarily
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
|
|
// Remove selection after a short delay
|
|
setTimeout(() => {
|
|
selection.removeAllRanges();
|
|
}, 1000);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignore highlight errors, not critical
|
|
console.log('Highlight error:', error);
|
|
}
|
|
}
|
|
|
|
exportMarkdown() {
|
|
const content = this.editor.innerText;
|
|
if (!content.trim()) {
|
|
this.showNotification('No content to export', 'warning');
|
|
return;
|
|
}
|
|
|
|
const blob = new Blob([content], { type: 'text/markdown' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'design-journal.md';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
this.showNotification('Markdown file exported', 'success');
|
|
}
|
|
|
|
async exportWeb() {
|
|
const content = this.editor.innerText;
|
|
if (!content.trim()) {
|
|
this.showNotification('No content to export', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.showNotification('Generating web export...', 'success');
|
|
|
|
const response = await fetch('/api/export/web', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content, title: 'Design Journal' })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Export failed');
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'design-journal-web.zip';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
this.showNotification('Web export (ZIP) downloaded', 'success');
|
|
} catch (error) {
|
|
console.error('Export error:', error);
|
|
this.showNotification('Error exporting web: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
importMarkdown(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
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
|
|
this.undoStack = [importedContent];
|
|
this.redoStack = [];
|
|
|
|
// Ensure editor is in edit mode
|
|
this.ensureEditMode();
|
|
|
|
this.showNotification('Markdown file imported', 'success');
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
toggleTheme() {
|
|
document.body.classList.toggle('dark-theme');
|
|
const isDark = document.body.classList.contains('dark-theme');
|
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
this.showNotification(`${isDark ? 'Dark' : 'Light'} mode activated`, 'success');
|
|
}
|
|
|
|
updateStatistics() {
|
|
const content = this.editor.innerText.trim();
|
|
|
|
// Count words
|
|
const words = content.split(/\s+/).filter(word => word.length > 0);
|
|
const wordCount = words.length;
|
|
|
|
// Count characters
|
|
const charCount = content.length;
|
|
|
|
// Calculate reading time (200 words per minute)
|
|
const readingTime = Math.ceil(wordCount / 200);
|
|
|
|
// Update DOM elements
|
|
const wordCountEl = document.getElementById('word-count');
|
|
const charCountEl = document.getElementById('char-count');
|
|
const readingTimeEl = document.getElementById('reading-time');
|
|
|
|
if (wordCountEl) wordCountEl.textContent = wordCount;
|
|
if (charCountEl) charCountEl.textContent = charCount;
|
|
if (readingTimeEl) readingTimeEl.textContent = `${readingTime} min`;
|
|
}
|
|
|
|
async openPresentMode() {
|
|
const content = this.editor.innerText;
|
|
|
|
if (!content.trim()) {
|
|
this.showNotification('No content to present', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.showNotification('Opening presentation mode...', 'success');
|
|
|
|
// Create a form to POST the content
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/present';
|
|
form.target = '_blank';
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'content';
|
|
input.value = content;
|
|
|
|
form.appendChild(input);
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
document.body.removeChild(form);
|
|
|
|
} catch (error) {
|
|
console.error('Present mode error:', error);
|
|
this.showNotification('Error opening presentation mode', 'error');
|
|
}
|
|
}
|
|
|
|
showShortcutsModal() {
|
|
const modalBody = document.getElementById('journal-modal-body');
|
|
const modalHeader = document.querySelector('#journal-modal .modal-header h3');
|
|
|
|
if (modalHeader) {
|
|
modalHeader.textContent = 'Keyboard Shortcuts';
|
|
}
|
|
|
|
modalBody.innerHTML = `
|
|
<div style="padding: 1rem;">
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="color: var(--primary-color); margin-bottom: 0.75rem;">Document Management</h4>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-weight: bold;">Ctrl+S</td>
|
|
<td style="padding: 0.5rem;">Save journal</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-weight: bold;">Ctrl+Z</td>
|
|
<td style="padding: 0.5rem;">Undo</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-weight: bold;">Ctrl+Y</td>
|
|
<td style="padding: 0.5rem;">Redo</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-weight: bold;">Tab</td>
|
|
<td style="padding: 0.5rem;">Indent text</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 0.5rem; font-weight: bold;">Esc</td>
|
|
<td style="padding: 0.5rem;">Close side panels</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div style="margin-top: 1.5rem;">
|
|
<h4 style="color: var(--primary-color); margin-bottom: 0.75rem;">Markdown Formatting</h4>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-family: monospace;"># Title</td>
|
|
<td style="padding: 0.5rem;">Heading level 1</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-family: monospace;">## Title</td>
|
|
<td style="padding: 0.5rem;">Heading level 2</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-family: monospace;">**bold**</td>
|
|
<td style="padding: 0.5rem;">Bold text</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-family: monospace;">*italic*</td>
|
|
<td style="padding: 0.5rem;">Italic text</td>
|
|
</tr>
|
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
<td style="padding: 0.5rem; font-family: monospace;">- item</td>
|
|
<td style="padding: 0.5rem;">Bullet list</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 0.5rem; font-family: monospace;">\`code\`</td>
|
|
<td style="padding: 0.5rem;">Inline code</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top: 2rem;">
|
|
<button class="btn primary" onclick="app.closeModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Show modal
|
|
const modal = document.getElementById('journal-modal');
|
|
modal.style.display = 'flex';
|
|
setTimeout(() => modal.classList.add('show'), 10);
|
|
}
|
|
|
|
async handleAI(action) {
|
|
const selection = window.getSelection().toString().trim();
|
|
const fullContent = this.editor.innerText;
|
|
|
|
// Disable all AI buttons to prevent API overload
|
|
this.disableAIButtons();
|
|
|
|
// Check conditions based on action
|
|
if (action === 'rephrase' && !selection) {
|
|
this.showAIFeedback('Please select text to rephrase');
|
|
this.enableAIButtons();
|
|
return;
|
|
}
|
|
|
|
// Determine content to use based on action
|
|
let contentToUse;
|
|
switch (action) {
|
|
case 'rephrase':
|
|
contentToUse = selection;
|
|
break;
|
|
case 'inconsistencies':
|
|
case 'duplications':
|
|
case 'advice':
|
|
case 'liberty':
|
|
contentToUse = selection || fullContent;
|
|
break;
|
|
default:
|
|
contentToUse = fullContent;
|
|
}
|
|
|
|
// Show loading message
|
|
this.showAIFeedback('<div class="feedback-message loading">Processing...</div>');
|
|
|
|
try {
|
|
let result;
|
|
|
|
switch (action) {
|
|
case 'rephrase':
|
|
result = await this.callAI('/api/ai/rephrase', { text: selection.trim(), context: fullContent.substring(0, 500) });
|
|
|
|
// Get rephrased text directly
|
|
const rephrasedText = result.rephrased || result.data || result;
|
|
|
|
// Store suggestion for validation
|
|
this.lastRephraseData = {
|
|
original: selection.trim(),
|
|
rephrased: rephrasedText,
|
|
selection: window.getSelection()
|
|
};
|
|
|
|
this.showAIFeedback(`
|
|
<strong>Rephrasing</strong><br><br>
|
|
|
|
<div style="background: var(--background-color); padding: 1rem; border-radius: 8px; margin: 0.5rem 0;">
|
|
<strong>Original text:</strong><br>
|
|
<em style="color: var(--text-light);">${selection.substring(0, 200)}${selection.length > 200 ? '...' : ''}</em>
|
|
</div>
|
|
|
|
<div style="background: var(--surface-color); border: 2px solid var(--success-color); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
<strong>Improved version:</strong><br>
|
|
${rephrasedText}
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<button id="validate-rephrase" class="btn success" style="margin-right: 0.5rem;">
|
|
Apply rephrasing
|
|
</button>
|
|
<button id="cancel-rephrase" class="btn secondary">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
`);
|
|
|
|
// Add event listeners for buttons
|
|
document.getElementById('validate-rephrase')?.addEventListener('click', () => {
|
|
this.validateRephrase();
|
|
this.enableAIButtons();
|
|
});
|
|
document.getElementById('cancel-rephrase')?.addEventListener('click', () => {
|
|
this.clearFeedback();
|
|
this.enableAIButtons();
|
|
});
|
|
break;
|
|
|
|
case 'inconsistencies':
|
|
result = await this.callAI('/api/ai/check-inconsistencies', { content: contentToUse });
|
|
this.showAIFeedback(`<strong>Inconsistency Analysis</strong><br><br>${this.formatAIResponse(result.analysis)}`);
|
|
break;
|
|
|
|
case 'duplications':
|
|
result = await this.callAI('/api/ai/check-duplications', { content: contentToUse });
|
|
this.showAIFeedback(`<strong>Duplication Check</strong><br><br>${this.formatAIResponse(result.analysis)}`);
|
|
break;
|
|
|
|
case 'advice':
|
|
result = await this.callAI('/api/ai/give-advice', { content: contentToUse, domain: 'design' });
|
|
this.showAIFeedback(`<strong>Improvement Advice</strong><br><br>${this.formatAIResponse(result.advice)}`);
|
|
break;
|
|
|
|
case 'liberty':
|
|
// Save state before AI modifications
|
|
this.saveState(true);
|
|
|
|
// Disable preview button during enhanced mode
|
|
const previewBtn = document.getElementById('preview-toggle');
|
|
if (previewBtn) {
|
|
previewBtn.disabled = true;
|
|
previewBtn.style.opacity = '0.5';
|
|
previewBtn.style.cursor = 'not-allowed';
|
|
}
|
|
|
|
const count = document.getElementById('liberty-repeat-count')?.value || 3;
|
|
const precision = document.getElementById('liberty-precision')?.value || 70;
|
|
|
|
// Initialize progress display
|
|
this.showAIFeedback(`
|
|
<div id="liberty-progress">
|
|
<strong>Enhanced Mode</strong><br>
|
|
<div style="margin: 0.5rem 0; font-size: 0.9rem; color: var(--text-light);">
|
|
${count} iterations - Precision: ${precision}% - Focus: design
|
|
</div>
|
|
<div class="progress-bar" style="width: 100%; height: 4px; background: var(--background-color); border-radius: 2px; margin: 1rem 0;">
|
|
<div id="liberty-progress-fill" style="height: 100%; background: var(--primary-color); border-radius: 2px; width: 0%; transition: width 0.3s ease;"></div>
|
|
</div>
|
|
<div id="liberty-iterations"></div>
|
|
</div>
|
|
`);
|
|
|
|
// Use EventSource for streaming
|
|
try {
|
|
await this.handleLibertyModeStreaming(fullContent, count, precision);
|
|
} finally {
|
|
// Re-enable preview button after completion (even on error)
|
|
if (previewBtn) {
|
|
previewBtn.disabled = false;
|
|
previewBtn.style.opacity = '1';
|
|
previewBtn.style.cursor = 'pointer';
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.error('AI Error:', error);
|
|
this.showAIFeedback(`<strong>Error</strong><br><br>An error occurred: ${error.message}<br><br>Check your connection and API configuration.`);
|
|
} finally {
|
|
// Re-enable all AI buttons after completion (even on error)
|
|
this.enableAIButtons();
|
|
}
|
|
}
|
|
|
|
async handleLibertyModeStreaming(content, iterations, precision) {
|
|
return new Promise((resolve, reject) => {
|
|
// Prepare data to send
|
|
const requestData = {
|
|
content: content,
|
|
iterations: iterations,
|
|
precision: precision,
|
|
focus: 'design'
|
|
};
|
|
|
|
// Create fetch request for streaming
|
|
fetch('/api/ai/liberty-mode', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP Error: ${response.status}`);
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
const processStream = () => {
|
|
return reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
// Decode received data
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
// Process each received line
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || ''; // Keep incomplete last line
|
|
|
|
for (const line of lines) {
|
|
if (line.trim().startsWith('data: ')) {
|
|
try {
|
|
const data = JSON.parse(line.slice(6)); // Remove "data: "
|
|
this.handleLibertyStreamData(data);
|
|
} catch (e) {
|
|
console.error('JSON parsing error:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
return processStream();
|
|
});
|
|
};
|
|
|
|
return processStream();
|
|
})
|
|
.catch(error => {
|
|
console.error('Streaming error:', error);
|
|
this.showAIFeedback(`<strong>Error</strong><br><br>Streaming error: ${error.message}`);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
handleLibertyStreamData(data) {
|
|
const progressFill = document.getElementById('liberty-progress-fill');
|
|
const iterationsDiv = document.getElementById('liberty-iterations');
|
|
|
|
if (data.error) {
|
|
// Display error
|
|
const errorHTML = `
|
|
<div style="background: var(--danger-color); color: white; padding: 1rem; border-radius: 8px; margin: 0.5rem 0;">
|
|
<strong>Error:</strong> ${data.error}
|
|
</div>
|
|
`;
|
|
iterationsDiv.innerHTML += errorHTML;
|
|
return;
|
|
}
|
|
|
|
if (data.iteration) {
|
|
// Update progress bar
|
|
const totalIterations = parseInt(document.getElementById('liberty-repeat-count')?.value || 3);
|
|
const progressPercent = (data.iteration / totalIterations) * 100;
|
|
|
|
if (progressFill) {
|
|
progressFill.style.width = `${progressPercent}%`;
|
|
}
|
|
|
|
// Display this iteration's explanation with selected section
|
|
const sectionBadge = data.selectedSection
|
|
? `<span style="display: inline-block; background: #2563eb; color: #ffffff; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.85rem; margin-left: 0.5rem; font-weight: 500;">${data.selectedSection}</span>`
|
|
: '';
|
|
|
|
const iterationHTML = `
|
|
<div style="background: var(--surface-color); border-left: 4px solid var(--primary-color); padding: 1rem; border-radius: 8px; margin: 0.5rem 0;">
|
|
<strong>Iteration ${data.iteration}</strong>${sectionBadge}<br><br>
|
|
${this.formatAIResponse(data.explanation)}
|
|
</div>
|
|
`;
|
|
|
|
iterationsDiv.innerHTML += iterationHTML;
|
|
|
|
// Update editor with new markdown if available
|
|
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
|
|
const feedback = document.getElementById('ai-assistant-feedback');
|
|
feedback.scrollTop = feedback.scrollHeight;
|
|
}
|
|
|
|
if (data.completed) {
|
|
// Finalize display
|
|
if (progressFill) {
|
|
progressFill.style.width = '100%';
|
|
}
|
|
|
|
if (data.finalMarkdown) {
|
|
// 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);
|
|
}
|
|
|
|
// Completion message
|
|
const completedHTML = `
|
|
<div style="background: var(--success-color); color: white; padding: 1rem; border-radius: 8px; margin: 1rem 0; text-align: center;">
|
|
<strong>Enhanced Mode completed!</strong><br>
|
|
${data.totalIterations || 'All'} iteration(s) completed
|
|
</div>
|
|
`;
|
|
|
|
iterationsDiv.innerHTML += completedHTML;
|
|
|
|
this.showNotification('Enhanced Mode completed', 'success');
|
|
}
|
|
}
|
|
|
|
async callAI(endpoint, data) {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Unknown error');
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
|
|
formatAIResponse(text) {
|
|
if (!text) return '';
|
|
|
|
// Parser la réponse pour séparer le raisonnement du contenu principal
|
|
const sections = text.split('##');
|
|
let formattedHTML = '';
|
|
|
|
const self = this;
|
|
sections.forEach((section, index) => {
|
|
if (index === 0 && section.trim()) {
|
|
// Première section sans titre
|
|
formattedHTML += `<div style="margin-bottom: 1rem;">${self.parseMarkdown(section.trim())}</div>`;
|
|
} else if (section.trim()) {
|
|
const lines = section.split('\n');
|
|
const title = lines[0].trim();
|
|
const content = lines.slice(1).join('\n').trim();
|
|
|
|
// Sections normales - avec markdown
|
|
formattedHTML += `
|
|
<div style="margin: 1rem 0;">
|
|
<strong style="color: var(--primary-color); font-size: 1.1em;">${title}</strong><br><br>
|
|
<div style="margin-top: 0.5rem;">${self.parseMarkdown(content)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
return formattedHTML || text;
|
|
}
|
|
|
|
parseMarkdown(text) {
|
|
if (!text) return '';
|
|
|
|
return text
|
|
// Titres H1-H6 (#, ##, ###, etc.)
|
|
.replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, title) => {
|
|
const level = hashes.length;
|
|
return `<h${level} style="color: var(--primary-color); margin: 1rem 0 0.5rem 0; font-weight: bold;">${title}</h${level}>`;
|
|
})
|
|
// Listes à puces (- ou *)
|
|
.replace(/^[\s]*[-\*]\s+(.+)$/gm, '<li style="margin: 0.25rem 0;">$1</li>')
|
|
// Listes numérotées
|
|
.replace(/^[\s]*\d+\.\s+(.+)$/gm, '<li style="margin: 0.25rem 0;">$1</li>')
|
|
// Code blocks avec ```
|
|
.replace(/```([\s\S]*?)```/g, '<pre style="background: var(--background-color); padding: 1rem; border-radius: 6px; border-left: 4px solid var(--primary-color); margin: 1rem 0; overflow-x: auto;"><code>$1</code></pre>')
|
|
// Citations avec >
|
|
.replace(/^>\s+(.+)$/gm, '<blockquote style="border-left: 4px solid var(--primary-color); padding-left: 1rem; margin: 1rem 0; font-style: italic; color: var(--text-light);">$1</blockquote>')
|
|
// Gras **texte**
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong style="color: var(--primary-color);">$1</strong>')
|
|
// Italique *texte*
|
|
.replace(/\*(.*?)\*/g, '<em style="color: var(--text-light);">$1</em>')
|
|
// Code inline `code`
|
|
.replace(/`([^`]+)`/g, '<code style="background: var(--surface-color); padding: 0.2rem 0.4rem; border-radius: 3px; font-family: monospace; font-size: 0.9em;">$1</code>')
|
|
// Liens [texte](url)
|
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color: var(--primary-color); text-decoration: underline;">$1</a>')
|
|
// Séparer les listes en blocs <ul> ou <ol>
|
|
.replace(/(<li[^>]*>.*<\/li>)/gs, (match) => {
|
|
if (match.includes('<li')) {
|
|
return `<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">${match}</ul>`;
|
|
}
|
|
return match;
|
|
})
|
|
// Sauts de ligne doubles pour paragraphes
|
|
.replace(/\n\n+/g, '\n\n')
|
|
.replace(/\n\n/g, '</p><p style="margin: 0.75rem 0; line-height: 1.6;">')
|
|
// Sauts de ligne simples
|
|
.replace(/\n/g, '<br>')
|
|
// Encapsuler dans un paragraphe si pas déjà fait
|
|
.replace(/^(?!<[h1-6|ul|ol|pre|blockquote])/i, '<p style="margin: 0.75rem 0; line-height: 1.6;">')
|
|
.replace(/$/i, '</p>');
|
|
}
|
|
|
|
showAIFeedback(message) {
|
|
const feedback = document.getElementById('ai-assistant-feedback');
|
|
feedback.innerHTML = `<div class="feedback-message">${message}</div>`;
|
|
// Scroll to top of feedback to see result
|
|
feedback.scrollTop = 0;
|
|
}
|
|
|
|
validateRephrase() {
|
|
if (!this.lastRephraseData) return;
|
|
|
|
try {
|
|
// Save state before rephrasing to allow undo
|
|
this.saveState(true);
|
|
|
|
// Replace text in editor
|
|
const range = this.lastRephraseData.selection.getRangeAt(0);
|
|
range.deleteContents();
|
|
range.insertNode(document.createTextNode(this.lastRephraseData.rephrased));
|
|
|
|
// Clear selection and regenerate TOC
|
|
window.getSelection().removeAllRanges();
|
|
this.generateTOC();
|
|
|
|
// Save state after rephrasing
|
|
this.saveState(true);
|
|
|
|
// Show success message
|
|
this.showNotification('Rephrasing applied successfully', 'success');
|
|
this.clearFeedback();
|
|
|
|
// Clean up data
|
|
this.lastRephraseData = null;
|
|
} catch (error) {
|
|
console.error('Error applying rephrasing:', error);
|
|
this.showNotification('Error applying rephrasing', 'error');
|
|
}
|
|
}
|
|
|
|
clearFeedback() {
|
|
const feedback = document.getElementById('ai-assistant-feedback');
|
|
feedback.innerHTML = `
|
|
<div class="feedback-message" style="text-align: center; color: var(--text-light);">
|
|
<strong>AI Assistant ready</strong><br>
|
|
Select text in the editor and click an action to begin.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
disableAIButtons() {
|
|
// Disable all AI buttons to prevent API overload
|
|
const aiButtons = [
|
|
'activate-rephrase',
|
|
'check-inconsistencies',
|
|
'check-duplications',
|
|
'give-advice',
|
|
'liberty-mode'
|
|
];
|
|
aiButtons.forEach(buttonId => {
|
|
const btn = document.getElementById(buttonId);
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.style.opacity = '0.5';
|
|
btn.style.cursor = 'not-allowed';
|
|
}
|
|
});
|
|
}
|
|
|
|
enableAIButtons() {
|
|
// Re-enable all AI buttons
|
|
const aiButtons = [
|
|
'activate-rephrase',
|
|
'check-inconsistencies',
|
|
'check-duplications',
|
|
'give-advice',
|
|
'liberty-mode'
|
|
];
|
|
aiButtons.forEach(buttonId => {
|
|
const btn = document.getElementById(buttonId);
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.style.opacity = '1';
|
|
btn.style.cursor = 'pointer';
|
|
}
|
|
});
|
|
}
|
|
|
|
ensureEditMode() {
|
|
// If in preview mode, force return to edit mode
|
|
if (this.isPreviewMode) {
|
|
const previewBtn = document.getElementById('preview-toggle');
|
|
|
|
// Return to edit mode without using originalContent because we want new content
|
|
this.editor.contentEditable = true;
|
|
this.editor.style.background = '';
|
|
this.editor.style.border = '';
|
|
this.editor.style.borderRadius = '';
|
|
|
|
// Change button
|
|
if (previewBtn) {
|
|
previewBtn.innerHTML = 'Preview';
|
|
previewBtn.classList.remove('secondary');
|
|
previewBtn.classList.add('primary');
|
|
}
|
|
|
|
this.isPreviewMode = false;
|
|
}
|
|
|
|
// Ensure editor is always editable
|
|
this.editor.contentEditable = true;
|
|
}
|
|
|
|
showNotification(message, type = 'success') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification ${type}`;
|
|
notification.textContent = message;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => notification.classList.add('show'), 100);
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
setTimeout(() => document.body.removeChild(notification), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
async togglePreview() {
|
|
const previewBtn = document.getElementById('preview-toggle');
|
|
const mainElement = document.querySelector('main');
|
|
|
|
if (!this.isPreviewMode) {
|
|
// Switch to preview mode - Save innerText instead of innerHTML
|
|
this.originalContent = this.editor.innerText;
|
|
const markdownContent = this.editor.innerText;
|
|
|
|
// Add preview-mode class to main to adjust grid layout
|
|
if (mainElement) {
|
|
mainElement.classList.add('preview-mode');
|
|
}
|
|
|
|
// Configure Marked for GitHub-compatible rendering
|
|
if (typeof marked !== 'undefined') {
|
|
marked.setOptions({
|
|
breaks: true,
|
|
gfm: true,
|
|
headerIds: true,
|
|
sanitize: false,
|
|
smartypants: false
|
|
});
|
|
}
|
|
|
|
// Convert Markdown to HTML with Marked (GitHub-compatible)
|
|
let previewHTML = '';
|
|
if (typeof marked !== 'undefined') {
|
|
previewHTML = marked.parse(markdownContent);
|
|
} else {
|
|
// Fallback to our custom parser if Marked is not loaded
|
|
previewHTML = this.parseMarkdown(markdownContent);
|
|
}
|
|
|
|
// Disable editing and apply GitHub preview style
|
|
this.editor.contentEditable = false;
|
|
this.editor.innerHTML = `<div class="markdown-preview">${previewHTML}</div>`;
|
|
this.editor.style.background = '';
|
|
this.editor.style.border = '';
|
|
this.editor.style.borderRadius = '';
|
|
|
|
// Disable AI buttons and save/load buttons in preview mode
|
|
const disabledButtons = [
|
|
'activate-rephrase',
|
|
'check-inconsistencies',
|
|
'check-duplications',
|
|
'give-advice',
|
|
'liberty-mode',
|
|
'save-journal',
|
|
'load-journal'
|
|
];
|
|
disabledButtons.forEach(buttonId => {
|
|
const btn = document.getElementById(buttonId);
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.style.opacity = '0.5';
|
|
btn.style.cursor = 'not-allowed';
|
|
}
|
|
});
|
|
|
|
// Process Mermaid diagrams after rendering
|
|
if (typeof mermaid !== 'undefined') {
|
|
try {
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
theme: document.body.classList.contains('dark-theme') ? 'dark' : 'default',
|
|
securityLevel: 'loose',
|
|
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
|
|
});
|
|
|
|
// Wait for DOM to be ready to process Mermaid diagrams
|
|
setTimeout(() => {
|
|
const preview = this.editor.querySelector('.markdown-preview');
|
|
if (preview) {
|
|
// Find all code blocks with 'mermaid' language
|
|
const mermaidBlocks = preview.querySelectorAll('code.language-mermaid, pre code.language-mermaid');
|
|
|
|
mermaidBlocks.forEach((block, index) => {
|
|
try {
|
|
const mermaidCode = block.textContent;
|
|
const uniqueId = `mermaid-${Date.now()}-${index}`;
|
|
|
|
// Create div with unique ID for Mermaid
|
|
const mermaidDiv = document.createElement('div');
|
|
mermaidDiv.id = uniqueId;
|
|
mermaidDiv.className = 'mermaid';
|
|
mermaidDiv.textContent = mermaidCode;
|
|
|
|
// Replace code block with Mermaid div
|
|
const pre = block.closest('pre') || block;
|
|
pre.parentNode.replaceChild(mermaidDiv, pre);
|
|
|
|
// Render diagram
|
|
mermaid.render(uniqueId + '-svg', mermaidCode).then(({ svg }) => {
|
|
mermaidDiv.innerHTML = svg;
|
|
}).catch((err) => {
|
|
console.warn('Mermaid rendering error:', err);
|
|
|
|
// Show error inline
|
|
mermaidDiv.innerHTML = `<pre style="color: var(--accent-color); padding: 1rem; background: var(--background-color); border-radius: 6px; border-left: 4px solid var(--accent-color);">Mermaid rendering error: ${err.message}\n\nPlease check the diagram syntax in the editor.</pre>`;
|
|
});
|
|
} catch (error) {
|
|
console.warn('Mermaid processing error:', error);
|
|
}
|
|
});
|
|
}
|
|
}, 200);
|
|
} catch (error) {
|
|
console.warn('Mermaid initialization error:', error);
|
|
}
|
|
}
|
|
|
|
// Change button
|
|
previewBtn.innerHTML = 'Edit';
|
|
previewBtn.classList.remove('primary');
|
|
previewBtn.classList.add('secondary');
|
|
|
|
this.isPreviewMode = true;
|
|
this.showNotification('Preview mode activated', 'success');
|
|
|
|
} else {
|
|
// Return to edit mode - Restore innerText instead of innerHTML
|
|
this.editor.contentEditable = true;
|
|
this.editor.innerText = this.originalContent;
|
|
this.editor.style.background = '';
|
|
this.editor.style.border = '';
|
|
this.editor.style.borderRadius = '';
|
|
|
|
// Remove preview-mode class to restore normal grid layout
|
|
if (mainElement) {
|
|
mainElement.classList.remove('preview-mode');
|
|
}
|
|
|
|
// Re-enable AI buttons and save/load buttons
|
|
const enabledButtons = [
|
|
'activate-rephrase',
|
|
'check-inconsistencies',
|
|
'check-duplications',
|
|
'give-advice',
|
|
'liberty-mode',
|
|
'save-journal',
|
|
'load-journal'
|
|
];
|
|
enabledButtons.forEach(buttonId => {
|
|
const btn = document.getElementById(buttonId);
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.style.opacity = '1';
|
|
btn.style.cursor = 'pointer';
|
|
}
|
|
});
|
|
|
|
// Change button
|
|
previewBtn.innerHTML = 'Preview';
|
|
previewBtn.classList.remove('secondary');
|
|
previewBtn.classList.add('primary');
|
|
|
|
this.isPreviewMode = false;
|
|
this.showNotification('Edit mode activated', 'success');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Side panel management
|
|
function togglePanel(side) {
|
|
const panel = document.getElementById(`${side}-panel`);
|
|
const overlay = document.getElementById('panel-overlay');
|
|
const otherPanel = document.getElementById(side === 'left' ? 'right-panel' : 'left-panel');
|
|
|
|
// Close the other panel if it's open
|
|
if (otherPanel && otherPanel.classList.contains('open')) {
|
|
otherPanel.classList.remove('open');
|
|
}
|
|
|
|
// Toggle current panel
|
|
if (panel.classList.contains('open')) {
|
|
panel.classList.remove('open');
|
|
overlay.classList.remove('active');
|
|
} else {
|
|
panel.classList.add('open');
|
|
overlay.classList.add('active');
|
|
}
|
|
}
|
|
|
|
function closeAllPanels() {
|
|
const leftPanel = document.getElementById('left-panel');
|
|
const rightPanel = document.getElementById('right-panel');
|
|
const overlay = document.getElementById('panel-overlay');
|
|
|
|
if (leftPanel) leftPanel.classList.remove('open');
|
|
if (rightPanel) rightPanel.classList.remove('open');
|
|
if (overlay) overlay.classList.remove('active');
|
|
}
|
|
|
|
// Close panels with Escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeAllPanels();
|
|
}
|
|
});
|
|
|
|
// Application initialization
|
|
let app;
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
app = new ConceptionAssistant();
|
|
|
|
// Load saved theme
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme === 'dark') {
|
|
document.body.classList.add('dark-theme');
|
|
}
|
|
|
|
// Initialize panels
|
|
initializePanels();
|
|
});
|
|
|
|
function initializePanels() {
|
|
// Initialize template management
|
|
initializeTemplateForm();
|
|
}
|
|
|
|
function initializeTemplateForm() {
|
|
const loadTemplateBtn = document.getElementById('load-template');
|
|
|
|
// Handle template loading
|
|
if (loadTemplateBtn) {
|
|
loadTemplateBtn.addEventListener('click', async () => {
|
|
try {
|
|
loadTemplateBtn.classList.add('loading');
|
|
|
|
const response = await fetch('/api/templates/default');
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// 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
|
|
app.undoStack = [result.data.content];
|
|
app.redoStack = [];
|
|
|
|
// Ensure editor is in edit mode
|
|
app.ensureEditMode();
|
|
|
|
app.showNotification('Template loaded successfully', 'success');
|
|
closeAllPanels();
|
|
} else {
|
|
app.showNotification('Error loading template', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
app.showNotification('Error loading template', 'error');
|
|
} finally {
|
|
loadTemplateBtn.classList.remove('loading');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Ensure togglePanel is globally accessible
|
|
window.togglePanel = togglePanel;
|