// 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', () => 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() { // Éviter de sauvegarder trop souvent (debounce) if (this.saveStateTimer) { clearTimeout(this.saveStateTimer); } this.saveStateTimer = setTimeout(() => { 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 = []; }, 1000); // Sauvegarder après 1 seconde d'inactivité } 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(); 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 `
Ajoutez des titres (# ## ###) à votre journal pour générer la table des matières.
🛑 Analyse terminée après ${result.iterations} itérations - Aucune amélioration évidente supplémentaire détectée.
`; } else { libertyHTML += `✅ Les ${result.iterations} itérations d'amélioration ont été automatiquement appliquées au document.
`; } result.results.forEach(iteration => { const borderColor = iteration.stopped ? 'var(--warning-color)' : 'var(--success-color)'; const icon = iteration.stopped ? '🛑' : '🎯'; libertyHTML += `$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 ')
// 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 = ` `; // Scroll vers le haut du feedback pour voir le résultat feedback.scrollTop = 0; } validateRephrase() { if (!this.lastRephraseData) return; try { // 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(); // 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 = ` `; } 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); } } // 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 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'); } }); } 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; // Convertir le Markdown en HTML joliment formaté const previewHTML = this.parseMarkdown(markdownContent); // Désactiver l'édition et appliquer le style preview this.editor.contentEditable = false; this.editor.innerHTML = `