Augustin b85e6d3a25 🔧 Améliorations navigation et historique
- Scroll précis vers sections table des matières avec surbrillance
- Historique Ctrl+Z/Y fonctionnel pour toutes les actions IA
- Mode édition automatique lors du chargement de fichiers
- Support CSS GitHub Preview pour visualisation Markdown

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 10:52:23 +02:00

1225 lines
41 KiB
JavaScript

// 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 `
<div class="journal-item-container">
<button class="journal-item btn secondary" data-id="${journal.id}">
<div class="journal-preview">
<strong>Journal ${journal.id}</strong>
<div>${preview}</div>
</div>
</button>
</div>
`;
}).join('');
const modalBody = document.getElementById('journal-modal-body');
modalBody.innerHTML = `
<div class="journal-list">
<button class="btn success mb-2" onclick="app.createNewJournal(); app.closeModal();" style="width: 100%;">
+ Nouveau journal
</button>
${journalList}
</div>
`;
// 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 = '<ul>';
let currentLevel = 0;
for (const item of toc) {
if (item.level > currentLevel) {
for (let i = currentLevel; i < item.level - 1; i++) {
tocHtml += '<ul>';
}
} else if (item.level < currentLevel) {
for (let i = item.level; i < currentLevel; i++) {
tocHtml += '</ul>';
}
}
tocHtml += `<li><a href="#${item.id}" onclick="app.scrollToHeading('${item.title}'); return false;">${item.title}</a></li>`;
currentLevel = item.level;
}
tocHtml += '</ul>';
} else {
tocHtml = '<div class="toc-placeholder"><p>Ajoutez des titres (# ## ###) à votre journal pour générer la table des matières.</p></div>';
}
document.getElementById('toc-nav').innerHTML = tocHtml;
// Restaurer la position du curseur
this.restoreSelection(savedRange);
}
addHeadingAnchors(lines, toc) {
const editorLines = this.editor.innerHTML.split('<br>');
let newContent = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const tocItem = toc.find(item => item.lineIndex === i);
if (tocItem) {
newContent += `<span class="heading-anchor" id="${tocItem.id}" style="position: absolute; margin-top: -100px; visibility: hidden;"></span>`;
}
newContent += line;
if (i < lines.length - 1) newContent += '<br>';
}
this.editor.innerHTML = newContent;
}
scrollToHeading(title) {
try {
// 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('<div class="feedback-message loading">🤖 Traitement en cours...</div>');
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(`
<strong>Reformulation</strong><br><br>
<div style="background: var(--background-color); padding: 1rem; border-radius: 8px; margin: 0.5rem 0;">
<strong>Texte original :</strong><br>
<em style="color: var(--text-light);">${selection.substring(0, 200)}${selection.length > 200 ? '...' : ''}</em>
</div>
<div style="background: var(--surface-color); border: 2px solid var(--success-color); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<strong>Version améliorée :</strong><br>
${rephrasedText}
</div>
<div style="text-align: center;">
<button id="validate-rephrase" class="btn success" style="margin-right: 0.5rem;">
Appliquer la reformulation
</button>
<button id="cancel-rephrase" class="btn secondary">
Annuler
</button>
</div>
`);
// 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(`<strong>Analyse des incohérences</strong><br><br>${this.formatAIResponse(result.analysis)}`);
break;
case 'duplications':
result = await this.callAI('/api/ai/check-duplications', { content: contentToUse });
this.showAIFeedback(`<strong>Vérification des doublons</strong><br><br>${this.formatAIResponse(result.analysis)}`);
break;
case 'advice':
result = await this.callAI('/api/ai/give-advice', { content: contentToUse, domain: 'conception' });
this.showAIFeedback(`<strong>Conseils d'amélioration</strong><br><br>${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;
// Mode liberté utilise toujours le document complet
result = await this.callAI('/api/ai/liberty-mode', { content: fullContent, iterations: count, focus: 'conception' });
// Utiliser le contenu final mis à jour par le backend
if (result.finalContent) {
this.editor.innerText = result.finalContent;
this.generateTOC();
// Sauvegarder l'état après les modifications de l'IA
this.saveState(true);
}
let libertyHTML = `<strong>Mode Liberté Intelligent (${result.iterations} itérations)</strong><br><br>`;
// Vérifier si l'IA s'est arrêtée prématurément
const lastIteration = result.results[result.results.length - 1];
if (lastIteration && lastIteration.stopped) {
libertyHTML += `<p>Analyse terminée après ${result.iterations} itérations - Aucune amélioration évidente supplémentaire détectée.</p>`;
} else {
libertyHTML += `<p>Les ${result.iterations} itérations d'amélioration ont été automatiquement appliquées au document.</p>`;
}
result.results.forEach(iteration => {
const borderColor = iteration.stopped ? 'var(--warning-color)' : 'var(--success-color)';
const icon = iteration.stopped ? 'STOP' : 'OK';
libertyHTML += `
<div style="background: var(--background-color); padding: 1rem; border-radius: 8px; margin: 0.5rem 0; border-left: 4px solid ${borderColor};">
<strong>${icon} Itération ${iteration.iteration} ${iteration.stopped ? '(Arrêt)' : '(Appliquée)'} :</strong><br><br>
${this.formatAIResponse(iteration.content)}
</div>
`;
});
this.showAIFeedback(libertyHTML);
break;
}
} catch (error) {
console.error('Erreur IA:', error);
this.showAIFeedback(`<strong>Erreur</strong><br><br>Une erreur s'est produite : ${error.message}<br><br>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 = '';
const self = this;
sections.forEach((section, index) => {
if (index === 0 && section.trim()) {
// Première section sans titre
formattedHTML += `<div style="margin-bottom: 1rem;">${self.parseMarkdown(section.trim())}</div>`;
} else if (section.trim()) {
const lines = section.split('\n');
const title = lines[0].trim();
const content = lines.slice(1).join('\n').trim();
// Sections normales - avec markdown
formattedHTML += `
<div style="margin: 1rem 0;">
<strong style="color: var(--primary-color); font-size: 1.1em;">${title}</strong><br><br>
<div style="margin-top: 0.5rem;">${self.parseMarkdown(content)}</div>
</div>
`;
}
});
return formattedHTML || text;
}
parseMarkdown(text) {
if (!text) return '';
return text
// Titres H1-H6 (#, ##, ###, etc.)
.replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, title) => {
const level = hashes.length;
return `<h${level} style="color: var(--primary-color); margin: 1rem 0 0.5rem 0; font-weight: bold;">${title}</h${level}>`;
})
// Listes à puces (- ou *)
.replace(/^[\s]*[-\*]\s+(.+)$/gm, '<li style="margin: 0.25rem 0;">$1</li>')
// Listes numérotées
.replace(/^[\s]*\d+\.\s+(.+)$/gm, '<li style="margin: 0.25rem 0;">$1</li>')
// Code blocks avec ```
.replace(/```([\s\S]*?)```/g, '<pre style="background: var(--background-color); padding: 1rem; border-radius: 6px; border-left: 4px solid var(--primary-color); margin: 1rem 0; overflow-x: auto;"><code>$1</code></pre>')
// Citations avec >
.replace(/^>\s+(.+)$/gm, '<blockquote style="border-left: 4px solid var(--primary-color); padding-left: 1rem; margin: 1rem 0; font-style: italic; color: var(--text-light);">$1</blockquote>')
// Gras **texte**
.replace(/\*\*(.*?)\*\*/g, '<strong style="color: var(--primary-color);">$1</strong>')
// Italique *texte*
.replace(/\*(.*?)\*/g, '<em style="color: var(--text-light);">$1</em>')
// Code inline `code`
.replace(/`([^`]+)`/g, '<code style="background: var(--surface-color); padding: 0.2rem 0.4rem; border-radius: 3px; font-family: monospace; font-size: 0.9em;">$1</code>')
// Liens [texte](url)
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color: var(--primary-color); text-decoration: underline;">$1</a>')
// Séparer les listes en blocs <ul> ou <ol>
.replace(/(<li[^>]*>.*<\/li>)/gs, (match) => {
if (match.includes('<li')) {
return `<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">${match}</ul>`;
}
return match;
})
// Sauts de ligne doubles pour paragraphes
.replace(/\n\n+/g, '\n\n')
.replace(/\n\n/g, '</p><p style="margin: 0.75rem 0; line-height: 1.6;">')
// Sauts de ligne simples
.replace(/\n/g, '<br>')
// Encapsuler dans un paragraphe si pas déjà fait
.replace(/^(?!<[h1-6|ul|ol|pre|blockquote])/i, '<p style="margin: 0.75rem 0; line-height: 1.6;">')
.replace(/$/i, '</p>');
}
showAIFeedback(message) {
const feedback = document.getElementById('ai-assistant-feedback');
feedback.innerHTML = `<div class="feedback-message">${message}</div>`;
// Scroll 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 = `
<div class="feedback-message" style="text-align: center; color: var(--text-light);">
<strong>Assistant IA prêt</strong><br>
Sélectionnez du texte dans l'éditeur et cliquez sur une action pour commencer.
</div>
`;
}
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 = `<div class="markdown-preview">${previewHTML}</div>`;
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 = `<pre style="color: var(--accent-color); padding: 1rem; background: var(--background-color); border-radius: 6px;">Erreur de rendu Mermaid: ${err.message}</pre>`;
});
} 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 = '<option value="">-- Choisir d\'abord un domaine --</option>';
loadTemplateBtn.disabled = true;
return;
}
// Activer le select de niveau
levelSelect.disabled = false;
levelSelect.innerHTML = `
<option value="">-- Choisir le niveau --</option>
<option value="simple">Simple</option>
<option value="detaille">Détaillé</option>
<option value="complet">Complet</option>
`;
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;