// Application principale class ConceptionAssistant { constructor() { this.currentJournalId = null; this.editor = null; this.undoStack = []; this.redoStack = []; this.tocTimer = null; 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()); // 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 `
`; }).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(); 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) { const content = this.editor.innerText; const lines = content.split('\n'); // Chercher la ligne qui correspond au titre for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#') && line.replace(/^#+\s*/, '') === title) { // Créer un range temporaire pour scroller vers cette ligne const range = document.createRange(); const selection = window.getSelection(); // Calculer la position approximative du caractère const beforeLines = lines.slice(0, i).join('\n'); const charPosition = beforeLines.length; try { // Créer un range au début de la ligne trouvée range.setStart(this.editor.childNodes[0] || this.editor, Math.min(charPosition, this.editor.textContent.length)); range.collapse(true); // Créer un élément temporaire pour le scroll const tempElement = document.createElement('span'); range.insertNode(tempElement); tempElement.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); // Nettoyer l'élément temporaire setTimeout(() => tempElement.remove(), 100); this.showNotification('Navigation vers la section', 'success'); return; } catch (error) { console.log('Erreur de scroll:', error); } } } this.showNotification('Section non trouvée', 'warning'); } 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) => { this.editor.innerText = e.target.result; this.generateTOC(); this.currentJournalId = null; // Nouveau journal 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': const count = document.getElementById('liberty-repeat-count')?.value || 3; // Mode liberté utilise toujours le document complet result = await this.callAI('/api/ai/liberty-mode', { content: fullContent, iterations: count, focus: 'conception' }); // Appliquer automatiquement chaque itération au document let updatedContent = fullContent; result.results.forEach(iteration => { updatedContent += '\n\n' + iteration.content; }); // Appliquer les changements directement this.editor.innerText = updatedContent; this.generateTOC(); this.saveState(); let libertyHTML = `🚀 Mode Liberté appliqué (${result.iterations} itérations)

`; libertyHTML += `

✅ Les ${result.iterations} itérations ont été automatiquement ajoutées au document.

`; result.results.forEach(iteration => { libertyHTML += `
Itération ${iteration.iteration} ajoutée :

${this.formatAIResponse(iteration.content)}
`; }); this.showAIFeedback(libertyHTML); 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 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 = ''; sections.forEach((section, index) => { if (index === 0 && section.trim()) { // Première section sans titre formattedHTML += `
${this.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 - pas de markdown formattedHTML += `
${title}

${content}
`; } }); return formattedHTML || text; } 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 { // 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 = `
🎯 Assistant IA prêt
Sélectionnez du texte dans l'éditeur et cliquez sur une action pour commencer.
`; } 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'); } }); } }