// Application principale 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(); } setupEditor() { this.editor = document.getElementById('journal-editor'); // Générer TOC avec debounce pour éviter de perturber la saisie this.editor.addEventListener('input', () => { this.debounceTOC(); }); // Gestion des raccourcis clavier this.editor.addEventListener('keydown', (e) => { // Ctrl+S pour sauvegarder if (e.ctrlKey && e.key === 's') { e.preventDefault(); this.saveJournal(); } // Ctrl+Z pour annuler if (e.ctrlKey && e.key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo(); } // Ctrl+Y ou Ctrl+Shift+Z pour refaire if (e.ctrlKey && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) { e.preventDefault(); this.redo(); } // Tab pour indentation if (e.key === 'Tab') { e.preventDefault(); document.execCommand('insertText', false, ' '); } }); // Sauvegarder l'état pour undo/redo avant modifications importantes this.editor.addEventListener('input', () => { this.saveState(); }); } showEditorPlaceholder() { // Le placeholder est maintenant géré via CSS ::before // Cette fonction peut être supprimée ou laissée vide pour compatibilité } setupEventListeners() { // Boutons de contrôle des journaux 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()); // Table des matières document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC()); // Export/Import document.getElementById('export-md')?.addEventListener('click', () => this.exportMarkdown()); document.getElementById('import-md')?.addEventListener('change', (e) => this.importMarkdown(e)); // Thème document.getElementById('theme-toggle')?.addEventListener('click', () => this.toggleTheme()); // Assistant IA (simulé pour le MVP) 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')); // Modal de chargement document.getElementById('close-journal-modal')?.addEventListener('click', () => this.closeModal()); // Fermer le modal en cliquant sur l'overlay document.getElementById('journal-modal')?.addEventListener('click', (e) => { if (e.target.id === 'journal-modal') { this.closeModal(); } }); // Bouton scroll to top document.getElementById('scroll-to-top')?.addEventListener('click', () => this.scrollToTop()); // Gestion de l'affichage du bouton scroll to top 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('Retour en haut', 'success'); } saveState(immediate = false) { // Si immediate est true, sauvegarder immédiatement sans debounce if (immediate) { this.performSaveState(); return; } // Éviter de sauvegarder trop souvent (debounce) if (this.saveStateTimer) { clearTimeout(this.saveStateTimer); } this.saveStateTimer = setTimeout(() => { this.performSaveState(); }, 1000); // Sauvegarder après 1 seconde d'inactivité } performSaveState() { const currentContent = this.editor.innerText; // Ne pas sauvegarder si le contenu n'a pas changé if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === currentContent) { return; } this.undoStack.push(currentContent); // Limiter la pile d'annulation à 50 éléments if (this.undoStack.length > 50) { this.undoStack.shift(); } // Vider la pile de refaire car on a fait une nouvelle 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; // Régénérer la table des matières this.generateTOC(); this.showNotification('Annulation effectuée', 'success'); } else { this.showNotification('Rien à annuler', 'warning'); } } redo() { if (this.redoStack.length > 0) { const nextContent = this.redoStack.pop(); this.undoStack.push(nextContent); this.editor.innerText = nextContent; // Régénérer la table des matières this.generateTOC(); this.showNotification('Rétablissement effectué', 'success'); } else { this.showNotification('Rien à rétablir', '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 = 'Sauvegarde...'; try { let response; if (this.currentJournalId) { // Mise à jour response = await fetch(`/api/journals/${this.currentJournalId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }); } else { // Création 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 = 'Sauvegardé'; this.showNotification('Journal sauvegardé avec succès', 'success'); setTimeout(() => statusEl.textContent = '', 3000); } else { throw new Error(result.error || 'Erreur de sauvegarde'); } } catch (error) { console.error('Erreur:', error); statusEl.textContent = 'Erreur'; this.showNotification('Erreur lors de la sauvegarde: ' + 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) { // Charger le dernier journal automatiquement const lastJournal = result.data[result.data.length - 1]; await this.loadJournal(lastJournal.id); } } catch (error) { console.error('Erreur chargement liste:', 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(); // Réinitialiser l'historique pour le nouveau journal this.undoStack = [journal.markdownContent]; this.redoStack = []; // S'assurer que l'éditeur est en mode édition this.ensureEditMode(); this.showNotification('Journal chargé', 'success'); } } catch (error) { console.error('Erreur chargement journal:', error); this.showNotification('Erreur lors du chargement', 'error'); } } async showJournalSelector() { try { const response = await fetch('/api/journals'); const result = await response.json(); if (result.success) { const journalList = result.data.map(journal => { // Extraire les premières lignes pour l'aperçu 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}
`; // Afficher le modal avec animation const modal = document.getElementById('journal-modal'); modal.style.display = 'flex'; setTimeout(() => modal.classList.add('show'), 10); // Ajouter les event listeners document.querySelectorAll('.journal-item').forEach(btn => { btn.addEventListener('click', () => { this.loadJournal(btn.dataset.id); this.closeModal(); }); }); } } catch (error) { console.error('Erreur:', error); this.showNotification('Erreur lors du chargement de la liste', '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(); // Réinitialiser l'historique pour le nouveau journal this.undoStack = ['']; this.redoStack = []; // S'assurer que l'éditeur est en mode édition this.ensureEditMode(); this.showNotification('Nouveau journal créé', 'success'); } debounceTOC() { if (this.tocTimer) { clearTimeout(this.tocTimer); } this.tocTimer = setTimeout(() => { this.generateTOC(); }, 500); // Attendre 500ms après la dernière frappe } 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); } } generateTOC() { // Sauvegarder la position du curseur const savedRange = this.saveSelection(); const content = this.editor.innerText; const lines = content.split('\n'); const toc = []; let tocHtml = ''; // Supprimer les ancres existantes 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 }); } } // Désactivé temporairement pour éviter de perturber la saisie // this.addHeadingAnchors(lines, toc); if (toc.length > 0) { tocHtml = ''; } else { tocHtml = '

Ajoutez des titres (# ## ###) à votre journal pour générer la table des matières.

'; } document.getElementById('toc-nav').innerHTML = tocHtml; // Restaurer la position du curseur 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 { // Méthode plus simple et robuste : chercher le texte directement dans l'éditeur const content = this.editor.innerText; const lines = content.split('\n'); // Trouver l'index de la ligne correspondant au titre let targetLineIndex = -1; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) { targetLineIndex = i; break; } } if (targetLineIndex === -1) { this.showNotification('Section non trouvée', 'warning'); return; } // Calculer la position approximative de la ligne const editorStyles = window.getComputedStyle(this.editor); const lineHeight = parseFloat(editorStyles.lineHeight) || 20; const paddingTop = parseFloat(editorStyles.paddingTop) || 0; // Calculer la position de scroll basée sur le numéro de ligne const targetScrollPosition = (targetLineIndex * lineHeight) + paddingTop; // Déterminer si l'éditeur ou la fenêtre doit être scrollée const editorRect = this.editor.getBoundingClientRect(); const editorHasScroll = this.editor.scrollHeight > this.editor.clientHeight; if (editorHasScroll) { // Scroller dans l'éditeur this.editor.scrollTo({ top: Math.max(0, targetScrollPosition - 60), behavior: 'smooth' }); } else { // Scroller la page entière const editorTop = this.editor.offsetTop; const windowScrollTarget = editorTop + targetScrollPosition - 100; window.scrollTo({ top: Math.max(0, windowScrollTarget), behavior: 'smooth' }); } // Optionnel : mettre en surbrillance temporairement le titre this.highlightHeading(title); this.showNotification(`Navigation vers: ${title}`, 'success'); } catch (error) { console.error('Erreur de scroll:', error); this.showNotification('Erreur lors de la navigation', 'error'); } } highlightHeading(title) { // Fonction pour mettre en surbrillance temporairement le titre trouvé 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) { // Créer un range pour sélectionner la ligne const selection = window.getSelection(); const range = document.createRange(); // Trouver le nœud texte et la 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) { // Trouver le début de la ligne dans ce nœud 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); // Sélectionner temporairement selection.removeAllRanges(); selection.addRange(range); // Retirer la sélection après un court délai setTimeout(() => { selection.removeAllRanges(); }, 1000); } } break; } } } catch (error) { // Ignorer les erreurs de surbrillance, ce n'est pas critique console.log('Erreur surbrillance:', error); } } exportMarkdown() { const content = this.editor.innerText; if (!content.trim()) { this.showNotification('Aucun contenu à exporter', '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 = 'journal-conception.md'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showNotification('Fichier Markdown exporté', 'success'); } 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; // Nouveau journal // Réinitialiser l'historique pour le fichier importé this.undoStack = [importedContent]; this.redoStack = []; // S'assurer que l'éditeur est en mode édition this.ensureEditMode(); this.showNotification('Fichier Markdown importé', '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(`Mode ${isDark ? 'sombre' : 'clair'} activé`, 'success'); } async handleAI(action) { const selection = window.getSelection().toString().trim(); const fullContent = this.editor.innerText; // Vérifier les conditions selon l'action if (action === 'rephrase' && !selection) { this.showAIFeedback('Veuillez sélectionner du texte à reformuler'); return; } // Déterminer le contenu à utiliser selon l'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; } // Afficher un message de chargement this.showAIFeedback('
🤖 Traitement en cours...
'); try { let result; switch (action) { case 'rephrase': result = await this.callAI('/api/ai/rephrase', { text: selection.trim(), context: fullContent.substring(0, 500) }); // Récupérer directement le texte reformulé const rephrasedText = result.rephrased || result.data || result; // Stocker la suggestion pour la validation this.lastRephraseData = { original: selection.trim(), rephrased: rephrasedText, selection: window.getSelection() }; this.showAIFeedback(` Reformulation

Texte original :
${selection.substring(0, 200)}${selection.length > 200 ? '...' : ''}
Version améliorée :
${rephrasedText}
`); // Ajouter les event listeners pour les boutons document.getElementById('validate-rephrase')?.addEventListener('click', () => this.validateRephrase()); document.getElementById('cancel-rephrase')?.addEventListener('click', () => this.clearFeedback()); break; case 'inconsistencies': result = await this.callAI('/api/ai/check-inconsistencies', { content: contentToUse }); this.showAIFeedback(`Analyse des incohérences

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

${this.formatAIResponse(result.analysis)}`); break; case 'advice': result = await this.callAI('/api/ai/give-advice', { content: contentToUse, domain: 'conception' }); this.showAIFeedback(`Conseils d'amélioration

${this.formatAIResponse(result.advice)}`); break; case 'liberty': // Sauvegarder l'état avant les modifications de l'IA this.saveState(true); const count = document.getElementById('liberty-repeat-count')?.value || 3; const precision = document.getElementById('liberty-precision')?.value || 70; // Initialiser l'affichage de progression this.showAIFeedback(`
🚀 Mode Liberté Total
${count} itérations • Précision: ${precision}% • Focus: conception
`); // Utiliser EventSource pour le streaming await this.handleLibertyModeStreaming(fullContent, count, precision); break; } } catch (error) { console.error('Erreur IA:', error); this.showAIFeedback(`Erreur

Une erreur s'est produite : ${error.message}

Vérifiez votre connexion et la configuration de l'API.`); } } async handleLibertyModeStreaming(content, iterations, precision) { return new Promise((resolve, reject) => { // Préparer les données à envoyer const requestData = { content: content, iterations: iterations, precision: precision, focus: 'conception' }; // Créer une requête fetch pour le 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(`Erreur HTTP: ${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; } // Décoder les données reçues buffer += decoder.decode(value, { stream: true }); // Traiter chaque ligne reçue const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Garder la dernière ligne incomplète for (const line of lines) { if (line.trim().startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); // Supprimer "data: " this.handleLibertyStreamData(data); } catch (e) { console.error('Erreur parsing JSON:', e); } } } return processStream(); }); }; return processStream(); }) .catch(error => { console.error('Erreur streaming:', error); this.showAIFeedback(`Erreur

Erreur de streaming: ${error.message}`); reject(error); }); }); } handleLibertyStreamData(data) { const progressFill = document.getElementById('liberty-progress-fill'); const iterationsDiv = document.getElementById('liberty-iterations'); if (data.error) { // Afficher l'erreur const errorHTML = `
❌ Erreur: ${data.error}
`; iterationsDiv.innerHTML += errorHTML; return; } if (data.iteration) { // Mettre à jour la barre de progression const totalIterations = parseInt(document.getElementById('liberty-repeat-count')?.value || 3); const progressPercent = (data.iteration / totalIterations) * 100; if (progressFill) { progressFill.style.width = `${progressPercent}%`; } // Afficher l'explication de cette itération const iterationHTML = `
🔄 Itération ${data.iteration}

${this.formatAIResponse(data.explanation)}
`; iterationsDiv.innerHTML += iterationHTML; // Mettre à jour l'éditeur avec le nouveau markdown si disponible if (data.markdown && data.markdown !== this.editor.innerText) { this.editor.innerText = data.markdown; this.generateTOC(); } // Scroller vers le bas du feedback pour voir la nouvelle itération const feedback = document.getElementById('ai-assistant-feedback'); feedback.scrollTop = feedback.scrollHeight; } if (data.completed) { // Finaliser l'affichage if (progressFill) { progressFill.style.width = '100%'; } if (data.finalMarkdown) { // S'assurer que le contenu final est bien dans l'éditeur this.editor.innerText = data.finalMarkdown; this.generateTOC(); // Sauvegarder l'état final this.saveState(true); } // Message de fin const completedHTML = `
✅ Mode Liberté Total terminé !
${data.totalIterations || 'Toutes les'} itération(s) complétée(s)
`; iterationsDiv.innerHTML += completedHTML; this.showNotification('Mode Liberté Total terminé', '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 || 'Erreur inconnue'); } 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 vers le haut du feedback pour voir le résultat feedback.scrollTop = 0; } validateRephrase() { if (!this.lastRephraseData) return; try { // Sauvegarder l'état avant la reformulation pour permettre l'undo this.saveState(true); // Remplacer le texte dans l'éditeur const range = this.lastRephraseData.selection.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(this.lastRephraseData.rephrased)); // Nettoyer la sélection et régénérer la TOC window.getSelection().removeAllRanges(); this.generateTOC(); // Sauvegarder l'état après la reformulation this.saveState(true); // Afficher un message de succès this.showNotification('Reformulation appliquée avec succès', 'success'); this.clearFeedback(); // Nettoyer les données this.lastRephraseData = null; } catch (error) { console.error('Erreur application reformulation:', error); this.showNotification('Erreur lors de l\'application de la reformulation', 'error'); } } clearFeedback() { const feedback = document.getElementById('ai-assistant-feedback'); feedback.innerHTML = `
    Assistant IA prêt
    Sélectionnez du texte dans l'éditeur et cliquez sur une action pour commencer.
    `; } ensureEditMode() { // Si on est en mode preview, forcer le retour en mode édition if (this.isPreviewMode) { const previewBtn = document.getElementById('preview-toggle'); // Revenir en mode édition sans utiliser originalContent car on veut le nouveau contenu this.editor.contentEditable = true; this.editor.style.background = ''; this.editor.style.border = ''; this.editor.style.borderRadius = ''; // Changer le bouton if (previewBtn) { previewBtn.innerHTML = 'Visualiser'; previewBtn.classList.remove('secondary'); previewBtn.classList.add('primary'); } this.isPreviewMode = false; } // S'assurer que l'éditeur est toujours éditable 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'); if (!this.isPreviewMode) { // Passer en mode prévisualisation this.originalContent = this.editor.innerHTML; const markdownContent = this.editor.innerText; // Configurer Marked pour un rendu compatible GitHub if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true, headerIds: true, sanitize: false, smartypants: false }); } // Convertir le Markdown en HTML avec Marked (compatible GitHub) let previewHTML = ''; if (typeof marked !== 'undefined') { previewHTML = marked.parse(markdownContent); } else { // Fallback vers notre parseur maison si Marked n'est pas chargé previewHTML = this.parseMarkdown(markdownContent); } // Désactiver l'édition et appliquer le style preview GitHub this.editor.contentEditable = false; this.editor.innerHTML = `
    ${previewHTML}
    `; this.editor.style.background = ''; this.editor.style.border = ''; this.editor.style.borderRadius = ''; // Traiter les diagrammes Mermaid après le rendu 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' }); // Attendre que le DOM soit prêt pour traiter les diagrammes Mermaid setTimeout(() => { const preview = this.editor.querySelector('.markdown-preview'); if (preview) { // Chercher tous les blocs de code avec la langue 'mermaid' 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}`; // Créer un div avec l'ID unique pour Mermaid const mermaidDiv = document.createElement('div'); mermaidDiv.id = uniqueId; mermaidDiv.className = 'mermaid'; mermaidDiv.textContent = mermaidCode; // Remplacer le bloc de code par le div Mermaid const pre = block.closest('pre') || block; pre.parentNode.replaceChild(mermaidDiv, pre); // Render le diagramme mermaid.render(uniqueId + '-svg', mermaidCode).then(({ svg }) => { mermaidDiv.innerHTML = svg; }).catch(err => { console.warn('Erreur rendu Mermaid:', err); mermaidDiv.innerHTML = `
    Erreur de rendu Mermaid: ${err.message}
    `; }); } catch (error) { console.warn('Erreur traitement Mermaid:', error); } }); } }, 200); } catch (error) { console.warn('Erreur initialisation Mermaid:', error); } } // Changer le bouton previewBtn.innerHTML = 'Éditer'; previewBtn.classList.remove('primary'); previewBtn.classList.add('secondary'); this.isPreviewMode = true; this.showNotification('Mode prévisualisation activé', 'success'); } else { // Revenir en mode édition this.editor.contentEditable = true; this.editor.innerHTML = this.originalContent; this.editor.style.background = ''; this.editor.style.border = ''; this.editor.style.borderRadius = ''; // Changer le bouton previewBtn.innerHTML = 'Visualiser'; previewBtn.classList.remove('secondary'); previewBtn.classList.add('primary'); this.isPreviewMode = false; this.showNotification('Mode édition activé', 'success'); } } } // Gestion des panneaux latéraux 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'); // Fermer l'autre panneau s'il est ouvert if (otherPanel && otherPanel.classList.contains('open')) { otherPanel.classList.remove('open'); } // Toggle le panneau actuel 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'); } // Fermer les panneaux avec Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeAllPanels(); } }); // Initialisation de l'application let app; document.addEventListener('DOMContentLoaded', () => { app = new ConceptionAssistant(); // Charger le thème sauvegardé const savedTheme = localStorage.getItem('theme'); if (savedTheme === 'dark') { document.body.classList.add('dark-theme'); } // Initialiser les panneaux initializePanels(); }); function initializePanels() { // Initialiser la gestion des templates initializeTemplateForm(); } function initializeTemplateForm() { const domainSelect = document.getElementById('domain-select'); const levelSelect = document.getElementById('level-select'); const loadTemplateBtn = document.getElementById('load-template'); // Gestion du changement de domaine if (domainSelect) { domainSelect.addEventListener('change', async () => { const domain = domainSelect.value; if (!domain) { levelSelect.disabled = true; levelSelect.innerHTML = ''; loadTemplateBtn.disabled = true; return; } // Activer le select de niveau levelSelect.disabled = false; levelSelect.innerHTML = ` `; loadTemplateBtn.disabled = true; }); } // Gestion du changement de niveau if (levelSelect) { levelSelect.addEventListener('change', async () => { const domain = domainSelect.value; const level = levelSelect.value; if (!domain || !level) { loadTemplateBtn.disabled = true; return; } // Activer le bouton de chargement directement loadTemplateBtn.disabled = false; }); } // Gestion du chargement du template if (loadTemplateBtn) { loadTemplateBtn.addEventListener('click', async () => { const domain = domainSelect.value; const level = levelSelect.value; if (!domain || !level) { app.showNotification('Veuillez sélectionner un domaine et un niveau', 'warning'); return; } try { loadTemplateBtn.classList.add('loading'); const response = await fetch(`/api/templates/${domain}/${level}`); const result = await response.json(); if (result.success) { // Charger le template dans l'éditeur app.editor.innerText = result.data.content; app.generateTOC(); app.currentJournalId = null; // Nouveau journal // Réinitialiser l'historique pour le nouveau template app.undoStack = [result.data.content]; app.redoStack = []; // S'assurer que l'éditeur est en mode édition app.ensureEditMode(); app.showNotification(`Template ${domain}/${level} chargé avec succès`, 'success'); closeAllPanels(); } else { app.showNotification('Erreur lors du chargement du template', 'error'); } } catch (error) { console.error('Erreur:', error); app.showNotification('Erreur lors du chargement du template', 'error'); } finally { loadTemplateBtn.classList.remove('loading'); } }); } } // S'assurer que togglePanel est accessible globalement window.togglePanel = togglePanel;