// Main application class ConceptionAssistant { constructor() { this.currentJournalId = null; this.editor = null; this.undoStack = []; this.redoStack = []; this.tocTimer = null; this.isPreviewMode = false; this.originalContent = ''; this.init(); } async init() { this.setupEditor(); this.setupEventListeners(); await this.loadJournalList(); 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(); }); // 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 = []; } 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'); } 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'); } 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; } 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(); // 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) { 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 `
`; }).join(''); const modalBody = document.getElementById('journal-modal-body'); modalBody.innerHTML = `
${journalList}
`; // 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); this.showNotification('Error loading list', 'error'); } } closeModal() { const modal = document.getElementById('journal-modal'); modal.classList.remove('show'); setTimeout(() => { modal.style.display = 'none'; }, 300); } createNewJournal() { this.currentJournalId = null; this.editor.innerText = ''; this.generateTOC(); this.clearFeedback(); // 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(); // 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 += ''; } } // Même niveau, fermer le li précédent else if (i > 0) { tocHtml += ''; } // Ajouter l'élément avec indentation CSS const indent = (item.level - 1) * 1; // 1rem par niveau const cleanTitle = this.cleanMarkdownFromTitle(item.title); tocHtml += `
  • ${cleanTitle}`; previousLevel = item.level; } // Fermer toutes les listes restantes for (let j = 0; j < previousLevel; j++) { tocHtml += '
  • '; } } else { tocHtml = '

    Add headings (# ## ###) to your journal to generate the table of contents.

    '; } document.getElementById('toc-nav').innerHTML = tocHtml; // Restore cursor position this.restoreSelection(savedRange); } addHeadingAnchors(lines, toc) { const editorLines = this.editor.innerHTML.split('
    '); 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 += ``; } newContent += line; if (i < lines.length - 1) newContent += '
    '; } 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.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 = `

    Document Management

    Ctrl+S Save journal
    Ctrl+Z Undo
    Ctrl+Y Redo
    Tab Indent text
    Esc Close side panels

    Markdown Formatting

    # Title Heading level 1
    ## Title Heading level 2
    **bold** Bold text
    *italic* Italic text
    - item Bullet list
    \`code\` Inline code
    `; // 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('
    Processing...
    '); 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(` Rephrasing

    Original text:
    ${selection.substring(0, 200)}${selection.length > 200 ? '...' : ''}
    Improved version:
    ${rephrasedText}
    `); // 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(`Inconsistency Analysis

    ${this.formatAIResponse(result.analysis)}`); break; case 'duplications': result = await this.callAI('/api/ai/check-duplications', { content: contentToUse }); this.showAIFeedback(`Duplication Check

    ${this.formatAIResponse(result.analysis)}`); break; case 'advice': result = await this.callAI('/api/ai/give-advice', { content: contentToUse, domain: 'design' }); this.showAIFeedback(`Improvement Advice

    ${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(`
    Enhanced Mode
    ${count} iterations - Precision: ${precision}% - Focus: design
    `); // 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(`Error

    An error occurred: ${error.message}

    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(`Error

    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 = `
    Error: ${data.error}
    `; 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 ? `${data.selectedSection}` : ''; const iterationHTML = `
    Iteration ${data.iteration}${sectionBadge}

    ${this.formatAIResponse(data.explanation)}
    `; 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(); } // 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(); // Save final state this.saveState(true); } // Completion message const completedHTML = `
    Enhanced Mode completed!
    ${data.totalIterations || 'All'} iteration(s) completed
    `; 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 += `
    ${self.parseMarkdown(section.trim())}
    `; } 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 += `
    ${title}

    ${self.parseMarkdown(content)}
    `; } }); 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 `${title}`; }) // Listes à puces (- ou *) .replace(/^[\s]*[-\*]\s+(.+)$/gm, '
  • $1
  • ') // Listes numérotées .replace(/^[\s]*\d+\.\s+(.+)$/gm, '
  • $1
  • ') // Code blocks avec ``` .replace(/```([\s\S]*?)```/g, '
    $1
    ') // Citations avec > .replace(/^>\s+(.+)$/gm, '
    $1
    ') // Gras **texte** .replace(/\*\*(.*?)\*\*/g, '$1') // Italique *texte* .replace(/\*(.*?)\*/g, '$1') // Code inline `code` .replace(/`([^`]+)`/g, '$1') // Liens [texte](url) .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // Séparer les listes en blocs `; } return match; }) // Sauts de ligne doubles pour paragraphes .replace(/\n\n+/g, '\n\n') .replace(/\n\n/g, '

    ') // Sauts de ligne simples .replace(/\n/g, '
    ') // Encapsuler dans un paragraphe si pas déjà fait .replace(/^(?!<[h1-6|ul|ol|pre|blockquote])/i, '

    ') .replace(/$/i, '

    '); } showAIFeedback(message) { const feedback = document.getElementById('ai-assistant-feedback'); feedback.innerHTML = `
    ${message}
    `; // 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 = `
    AI Assistant ready
    Select text in the editor and click an action to begin.
    `; } 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 = `
    ${previewHTML}
    `; 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 = `
    Mermaid rendering error: ${err.message}\n\nPlease check the diagram syntax in the editor.
    `; }); } 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.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;