
🎯 Améliorations UX critiques : - Fix curseur qui revenait au début lors de la saisie - Suppression autosauvegarde automatique - Centrage flèche bouton scroll-to-top - Mode liberté applique automatiquement les itérations 🤖 IA optimisée : - Migration vers mistral-medium classique - Suppression raisonnement IA pour réponses directes - Prompt reformulation strict (texte seul) - Routes IA complètes fonctionnelles 📚 Templates professionnels complets : - Structure 12 sections selon standards académiques/industrie - 6 domaines : informatique, math, business, design, recherche, ingénierie - 3 niveaux : simple (9 sections), détaillé, complet (12 sections) - Méthodologies spécialisées par domaine ✨ Nouvelles fonctionnalités : - Debounce TOC pour performance saisie - Navigation sections améliorée - Sauvegarde/restauration position curseur 🧠 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
897 lines
28 KiB
JavaScript
897 lines
28 KiB
JavaScript
// 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 `
|
|
<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();
|
|
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) {
|
|
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('<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':
|
|
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 = `<strong>🚀 Mode Liberté appliqué (${result.iterations} itérations)</strong><br><br>`;
|
|
libertyHTML += `<p>✅ Les ${result.iterations} itérations ont été automatiquement ajoutées au document.</p>`;
|
|
result.results.forEach(iteration => {
|
|
libertyHTML += `
|
|
<div style="background: var(--background-color); padding: 1rem; border-radius: 8px; margin: 0.5rem 0; border-left: 4px solid var(--success-color);">
|
|
<strong>Itération ${iteration.iteration} ajouté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 = '';
|
|
|
|
sections.forEach((section, index) => {
|
|
if (index === 0 && section.trim()) {
|
|
// Première section sans titre
|
|
formattedHTML += `<div style="margin-bottom: 1rem;">${this.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 - pas de markdown
|
|
formattedHTML += `
|
|
<div style="margin: 1rem 0;">
|
|
<strong>${title}</strong><br><br>
|
|
${content}
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
return formattedHTML || text;
|
|
}
|
|
|
|
|
|
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 {
|
|
// 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 = `
|
|
<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>
|
|
`;
|
|
}
|
|
|
|
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 = '<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
|
|
|
|
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');
|
|
}
|
|
});
|
|
}
|
|
|
|
} |