diff --git a/lib/generation/InitialGeneration.js b/lib/generation/InitialGeneration.js index daac8bc..3ef832c 100644 --- a/lib/generation/InitialGeneration.js +++ b/lib/generation/InitialGeneration.js @@ -15,7 +15,7 @@ const { chunkArray, sleep } = require('../selective-enhancement/SelectiveUtils') class InitialGenerationLayer { constructor() { this.name = 'InitialGeneration'; - this.defaultLLM = 'claude'; + this.defaultLLM = 'claude-sonnet-4-5'; this.priority = 0; // Priorité maximale - appliqué en premier } @@ -131,8 +131,16 @@ class InitialGenerationLayer { */ detectElementType(tag) { const tagLower = tag.toLowerCase(); - - if (tagLower.includes('titre') || tagLower.includes('h1') || tagLower.includes('h2')) { + + // 🔥 FIX: Vérifier d'abord les suffixes _title vs _text pour éviter confusion + if (tagLower.endsWith('_title')) { + return 'titre'; + } else if (tagLower.endsWith('_text')) { + return 'contenu'; + } + + // Legacy patterns (pour compatibilité) + if (tagLower.includes('titre_h') || tagLower === 'titre_h1' || tagLower.startsWith('titre_')) { return 'titre'; } else if (tagLower.includes('intro') || tagLower.includes('introduction')) { return 'introduction'; @@ -140,6 +148,8 @@ class InitialGenerationLayer { return 'conclusion'; } else if (tagLower.includes('faq') || tagLower.includes('question')) { return 'faq'; + } else if (tagLower.startsWith('txt_') || tagLower.includes('_text')) { + return 'contenu'; } else { return 'contenu'; } diff --git a/lib/selective-enhancement/SelectiveUtils.js b/lib/selective-enhancement/SelectiveUtils.js index 4b50375..3e9b753 100644 --- a/lib/selective-enhancement/SelectiveUtils.js +++ b/lib/selective-enhancement/SelectiveUtils.js @@ -154,7 +154,12 @@ function analyzeStyleConsistency(content, expectedPersonality = null) { // 1. Analyser alignement personnalité if (expectedPersonality && expectedPersonality.vocabulairePref) { - const personalityWords = expectedPersonality.vocabulairePref.toLowerCase().split(','); + // Convertir en string si ce n'est pas déjà le cas + const vocabPref = typeof expectedPersonality.vocabulairePref === 'string' + ? expectedPersonality.vocabulairePref + : String(expectedPersonality.vocabulairePref); + + const personalityWords = vocabPref.toLowerCase().split(','); const contentLower = content.toLowerCase(); personalityWords.forEach(word => { @@ -483,15 +488,307 @@ function formatDuration(ms) { * GÉNÉRATION SIMPLE (REMPLACE CONTENTGENERATION.JS) */ +/** + * Détecter le type d'élément (legacy InitialGeneration.js logic) + * Retourne un string simple : 'titre', 'intro', 'paragraphe', 'faq_question', 'faq_reponse', 'conclusion' + */ +function detectElementType(tag) { + const tagLower = tag.toLowerCase(); + + // 🔥 FIX: Vérifier d'abord les préfixes de type spécifique (Intro_, Titre_, Txt_) + // avant les suffixes génériques (_title, _text) + + // Intro_H2_1, Intro_H3_5, etc. → type 'intro' + if (tagLower.startsWith('intro_')) { + return 'intro'; + } + + // Titre_H2_1, Titre_H3_5, etc. → type 'titre' + if (tagLower.startsWith('titre_')) { + return 'titre'; + } + + // Txt_H2_1, Txt_H3_5, etc. → type 'paragraphe' + if (tagLower.startsWith('txt_')) { + return 'paragraphe'; + } + + // Conclusion_* → type 'conclusion' + if (tagLower.startsWith('conclu') || tagLower.includes('c_1') || tagLower === 'c1') { + return 'conclusion'; + } + + // FAQ + if (tagLower.includes('faq') || tagLower.includes('question') || tagLower.startsWith('q_') || tagLower.startsWith('q-')) { + return 'faq_question'; + } + + if (tagLower.includes('answer') || tagLower.includes('réponse') || tagLower.includes('reponse') || tagLower.startsWith('a_') || tagLower.startsWith('a-')) { + return 'faq_reponse'; + } + + // Suffixes génériques pour format alternatif (H2_1_title, H2_1_text) + // À vérifier APRÈS les préfixes pour éviter les conflits + if (tagLower.endsWith('_title')) { + return 'titre'; + } else if (tagLower.endsWith('_text')) { + return 'paragraphe'; + } + + // Paragraphes (défaut) + return 'paragraphe'; +} + +/** + * Détecter contrainte de longueur dans une instruction + * Retourne { hasConstraint: boolean, constraint: string|null } + */ +function detectLengthConstraintInInstruction(instruction) { + if (!instruction) return { hasConstraint: false, constraint: null }; + + const lowerInstr = instruction.toLowerCase(); + + // Patterns de contraintes : "X mots", "X-Y mots", "environ X mots", "maximum X mots" + const patterns = [ + /(\d+)\s*-\s*(\d+)\s*mots?/i, // "80-200 mots" + /environ\s+(\d+)\s*mots?/i, // "environ 100 mots" + /maximum\s+(\d+)\s*mots?/i, // "maximum 25 mots" + /minimum\s+(\d+)\s*mots?/i, // "minimum 50 mots" + /(\d+)\s+mots?\s+(maximum|minimum)/i, // "25 mots maximum" + /^(\d+)\s*mots?$/i, // "25 mots" seul + /\b(\d+)\s*mots?\b/i // "X mots" quelque part + ]; + + for (const pattern of patterns) { + const match = instruction.match(pattern); + if (match) { + return { hasConstraint: true, constraint: match[0] }; + } + } + + return { hasConstraint: false, constraint: null }; +} + +/** + * Créer un prompt adapté au type d'élément avec contraintes de longueur (legacy logic) + * @param {string} associatedTitle - Titre généré précédemment pour les textes/intros (important pour cohérence) + */ +function createTypedPrompt(tag, type, instruction, csvData, associatedTitle = null) { + const keyword = csvData.mc0 || ''; + const title = csvData.t0 || ''; + const personality = csvData.personality; + + // 🔥 NOUVEAU : Détecter si l'instruction contient déjà une contrainte de longueur + const instructionConstraint = detectLengthConstraintInInstruction(instruction); + + // 📊 LOG: Afficher détection contrainte + if (instructionConstraint.hasConstraint) { + logSh(` 🔍 Contrainte détectée dans instruction: "${instructionConstraint.constraint}"`, 'DEBUG'); + } else { + logSh(` ⚙️ Aucune contrainte détectée, utilisation contrainte type "${type}"`, 'DEBUG'); + } + + let lengthConstraint = ''; + let specificInstructions = ''; + + // Si l'instruction a déjà une contrainte, ne pas en ajouter une autre + if (instructionConstraint.hasConstraint) { + lengthConstraint = `RESPECTE STRICTEMENT la contrainte de longueur indiquée dans l'instruction : "${instructionConstraint.constraint}"`; + + // Instructions génériques selon type (sans répéter la longueur) + switch (type) { + case 'titre': + specificInstructions = `Le titre doit être: +- COURT et PERCUTANT +- Pas de phrases complètes +- Intégrer "${keyword}"`; + break; + + case 'intro': + specificInstructions = `L'introduction doit: +- Présenter le sujet +- Accrocher le lecteur`; + break; + + case 'conclusion': + specificInstructions = `La conclusion doit: +- Résumer les points clés +- Appel à l'action si pertinent`; + break; + + case 'faq_question': + specificInstructions = `La question FAQ doit être: +- Courte et directe +- Formulée du point de vue utilisateur`; + break; + + case 'faq_reponse': + specificInstructions = `La réponse FAQ doit être: +- Directe et informative +- Répondre précisément à la question`; + break; + + case 'paragraphe': + default: + specificInstructions = `Le paragraphe doit: +- Développer un aspect du sujet +- Contenu informatif et engageant`; + break; + } + } else { + // Pas de contrainte dans l'instruction → utiliser les contraintes par défaut du type + switch (type) { + case 'titre': + lengthConstraint = '8-15 mots MAXIMUM'; + specificInstructions = `Le titre doit être: +- COURT et PERCUTANT (8-15 mots max) +- Pas de phrases complètes +- Intégrer "${keyword}"`; + break; + + case 'intro': + lengthConstraint = '40-80 mots (2-3 phrases courtes)'; + specificInstructions = `L'introduction doit: +- Présenter le sujet +- Accrocher le lecteur +- 40-80 mots seulement`; + break; + + case 'conclusion': + lengthConstraint = '40-80 mots (2-3 phrases courtes)'; + specificInstructions = `La conclusion doit: +- Résumer les points clés +- Appel à l'action si pertinent +- 40-80 mots seulement`; + break; + + case 'faq_question': + lengthConstraint = '10-20 mots'; + specificInstructions = `La question FAQ doit être: +- Courte et directe (10-20 mots) +- Formulée du point de vue utilisateur`; + break; + + case 'faq_reponse': + lengthConstraint = '60-120 mots (3-5 phrases)'; + specificInstructions = `La réponse FAQ doit être: +- Directe et informative (60-120 mots) +- Répondre précisément à la question`; + break; + + case 'paragraphe': + default: + lengthConstraint = '80-200 mots (3-6 phrases)'; + specificInstructions = `Le paragraphe doit: +- Développer un aspect du sujet +- Contenu informatif et engageant +- 80-200 mots (PAS PLUS)`; + break; + } + } + + // 🔥 NOUVEAU : Injecter le titre associé pour textes/intros + let titleContext = ''; + if (associatedTitle && (type === 'intro' || type === 'paragraphe')) { + titleContext = `\n🎯 TITRE ASSOCIÉ (IMPORTANT - utilise-le comme base): "${associatedTitle}"\n⚠️ CRUCIAL: Le contenu doit développer et être cohérent avec ce titre spécifique.\n`; + } + + // 🔥 Helper : Sélectionner aléatoirement max N éléments d'un array + const selectRandomItems = (arr, max = 2) => { + if (!Array.isArray(arr) || arr.length === 0) return arr; + if (arr.length <= max) return arr; + + // Fisher-Yates shuffle puis prendre les N premiers + const shuffled = [...arr]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + return shuffled.slice(0, max); + }; + + // 🔥 NOUVEAU : Contexte personnalité enrichi + // ⚠️ EXCLUSION : Pas de personnalité pour les questions FAQ (seulement pour les réponses) + let personalityContext = ''; + const includePersonality = personality && type !== 'faq_question'; + + if (includePersonality) { + // 🎲 Sélection aléatoire de 2 éléments max pour D, E, F, J + const vocabArray = Array.isArray(personality.vocabulairePref) + ? personality.vocabulairePref + : (personality.vocabulairePref || '').split(',').map(s => s.trim()).filter(s => s); + const vocabList = selectRandomItems(vocabArray, 2).join(', '); + + const connecteursArray = Array.isArray(personality.connecteursPref) + ? personality.connecteursPref + : (personality.connecteursPref || '').split(',').map(s => s.trim()).filter(s => s); + const connecteursList = selectRandomItems(connecteursArray, 2).join(', '); + + const motsClésArray = Array.isArray(personality.motsClesSecteurs) + ? personality.motsClesSecteurs + : (personality.motsClesSecteurs || '').split(',').map(s => s.trim()).filter(s => s); + const motsClesList = selectRandomItems(motsClésArray, 2).join(', '); + + const ctaArray = Array.isArray(personality.ctaStyle) + ? personality.ctaStyle + : (personality.ctaStyle || '').split(',').map(s => s.trim()).filter(s => s); + const ctaList = selectRandomItems(ctaArray, 2).join(', '); + + personalityContext = ` +PROFIL PERSONNALITÉ RÉDACTEUR: +- Nom: ${personality.nom || 'Standard'} +- Profil: ${personality.description || 'Expert généraliste'} +- Style: ${personality.style || 'professionnel'} +${motsClesList ? `- Secteurs d'expertise: ${motsClesList}` : ''} +${vocabList ? `- Vocabulaire préféré: ${vocabList}` : ''} +${connecteursList ? `- Connecteurs préférés: ${connecteursList}` : ''} +${personality.longueurPhrases ? `- Longueur phrases: ${personality.longueurPhrases}` : ''} +${personality.niveauTechnique ? `- Niveau technique: ${personality.niveauTechnique}` : ''} +${ctaList ? `- Style CTA: ${ctaList}` : ''} +`; + } + + const prompt = `Tu es un rédacteur SEO expert. Génère du contenu professionnel et naturel. + +CONTEXTE: +- Sujet principal: ${keyword} +- Titre de l'article: ${title} +${personalityContext}${titleContext} +ÉLÉMENT À GÉNÉRER: ${tag} (Type: ${type}) + +INSTRUCTION SPÉCIFIQUE: +${instruction} + +CONTRAINTE DE LONGUEUR (⚠️ CRUCIAL - À RESPECTER ABSOLUMENT): +${lengthConstraint} + +${specificInstructions} + +CONSIGNES RÉDACTIONNELLES: +${includePersonality ? `- ADOPTE le style et vocabulaire du profil personnalité ci-dessus +- Utilise les connecteurs préférés listés pour fluidifier le texte +- Adapte la longueur des phrases selon le profil (${personality?.longueurPhrases || 'moyennes'}) +- Niveau technique: ${personality?.niveauTechnique || 'moyen'}` : '- Formulation neutre et professionnelle (question FAQ)'} +- Ton naturel et humain, pas robotique +- Intégration fluide du mot-clé "${keyword}" +${associatedTitle ? `- DÉVELOPPE spécifiquement le titre: "${associatedTitle}"` : ''} +- PAS de formatage markdown (ni **, ni ##, ni -) +- PAS de préambule ou conclusion ajoutée +- ⚠️ IMPÉRATIF: RESPECTE la contrainte de longueur indiquée ci-dessus + +RÉPONSE (contenu uniquement, sans intro comme "Voici le contenu..."):`; + + return prompt; +} + /** * Génération simple avec LLM configurable (compatible avec l'ancien système) */ async function generateSimple(hierarchy, csvData, options = {}) { const LLMManager = require('../LLMManager'); + const llmProvider = options.llmProvider || 'claude-sonnet-4-5'; - const llmProvider = options.llmProvider || 'claude'; - - logSh(`🔥 Génération simple avec ${llmProvider.toUpperCase()}`, 'INFO'); + logSh(`🔥 Génération avec contraintes de longueur par type (${llmProvider.toUpperCase()})`, 'INFO'); if (!hierarchy || Object.keys(hierarchy).length === 0) { throw new Error('Hiérarchie vide ou invalide'); @@ -506,142 +803,302 @@ async function generateSimple(hierarchy, csvData, options = {}) { llmProvider: llmProvider } }; - + const startTime = Date.now(); - + + // Fonction utilitaire pour résoudre les variables + const resolveVariables = (text, csvData) => { + return text.replace(/\{\{?([^}]+)\}?\}/g, (match, variable) => { + const cleanVar = variable.trim(); + + if (cleanVar === 'MC0') return csvData.mc0 || ''; + if (cleanVar === 'T0') return csvData.t0 || ''; + if (cleanVar === 'T-1') return csvData.tMinus1 || ''; + if (cleanVar === 'L-1') return csvData.lMinus1 || ''; + + if (cleanVar.startsWith('MC+1_')) { + const index = parseInt(cleanVar.split('_')[1]) - 1; + const mcPlus1 = (csvData.mcPlus1 || '').split(',').map(s => s.trim()); + return mcPlus1[index] || csvData.mc0 || ''; + } + + if (cleanVar.startsWith('T+1_')) { + const index = parseInt(cleanVar.split('_')[1]) - 1; + const tPlus1 = (csvData.tPlus1 || '').split(',').map(s => s.trim()); + return tPlus1[index] || csvData.t0 || ''; + } + + if (cleanVar.startsWith('L+1_')) { + const index = parseInt(cleanVar.split('_')[1]) - 1; + const lPlus1 = (csvData.lPlus1 || '').split(',').map(s => s.trim()); + return lPlus1[index] || ''; + } + + return csvData.mc0 || ''; + }); + }; + + // Fonction pour extraire l'instruction de l'élément + const extractInstruction = (tag, item) => { + if (typeof item === 'string') return item; + if (item.instructions) return item.instructions; + if (item.title && item.title.instructions) return item.title.instructions; + if (item.text && item.text.instructions) return item.text.instructions; + + if (item.questions && Array.isArray(item.questions) && item.questions.length > 0) { + const faqItem = item.questions[0]; + if (faqItem.originalElement && faqItem.originalElement.resolvedContent) { + return faqItem.originalElement.resolvedContent; + } + return `Générer une ${tag.startsWith('q') ? 'question' : 'réponse'} FAQ pertinente sur ${csvData.mc0}`; + } + + return `Générer du contenu pertinent pour la section ${tag} sur "${csvData.mc0}"`; + }; + try { - // Générer chaque élément avec Claude - for (const [tag, item] of Object.entries(hierarchy)) { - try { - logSh(`🎯 Génération: ${tag}`, 'DEBUG'); + // Grouper éléments par couples (titre/texte et FAQ) + const batches = []; - // Extraire l'instruction correctement selon la structure - let instruction = ''; - if (typeof item === 'string') { - instruction = item; - } else if (item.instructions) { - instruction = item.instructions; - } else if (item.title && item.title.instructions) { - instruction = item.title.instructions; - } else if (item.text && item.text.instructions) { - instruction = item.text.instructions; - } else { - logSh(`⚠️ Pas d'instruction trouvée pour ${tag}, structure: ${JSON.stringify(Object.keys(item))}`, 'WARNING'); - continue; // Skip cet élément + for (const [sectionKey, section] of Object.entries(hierarchy)) { + const batch = []; + + // Couple titre + texte + if (section.title && section.text) { + // 🔥 FIX: Utiliser le nom original pour préserver le type (Intro_, Txt_, etc.) + const titleTag = section.title.originalElement?.name || `${sectionKey}_title`; + const textTag = section.text.originalElement?.name || `${sectionKey}_text`; + + batch.push({ tag: titleTag, item: section.title, isCouple: 'titre' }); + batch.push({ tag: textTag, item: section.text, isCouple: 'texte' }); + } else if (section.title) { + const tag = section.title.originalElement?.name || sectionKey; + batch.push({ tag: tag, item: section.title, isCouple: null }); + } else if (section.text) { + const tag = section.text.originalElement?.name || sectionKey; + batch.push({ tag: tag, item: section.text, isCouple: null }); + } + + // Paires FAQ (q_1 + a_1, q_2 + a_2, etc.) + if (section.questions && section.questions.length > 0) { + for (let i = 0; i < section.questions.length; i += 2) { + const question = section.questions[i]; + const answer = section.questions[i + 1]; + + if (question) { + batch.push({ + tag: question.hierarchyPath || `faq_q_${i}`, + item: question, + isCouple: 'faq_question' + }); + } + + if (answer) { + batch.push({ + tag: answer.hierarchyPath || `faq_a_${i}`, + item: answer, + isCouple: 'faq_reponse' + }); + } } + } - // Fonction pour résoudre les variables dans les instructions - const resolveVariables = (text, csvData) => { - return text.replace(/\{\{?([^}]+)\}?\}/g, (match, variable) => { - const cleanVar = variable.trim(); - - // Variables simples - if (cleanVar === 'MC0') return csvData.mc0 || ''; - if (cleanVar === 'T0') return csvData.t0 || ''; - if (cleanVar === 'T-1') return csvData.tMinus1 || ''; - if (cleanVar === 'L-1') return csvData.lMinus1 || ''; - - // Variables avec index MC+1_X - if (cleanVar.startsWith('MC+1_')) { - const index = parseInt(cleanVar.split('_')[1]) - 1; - const mcPlus1 = (csvData.mcPlus1 || '').split(',').map(s => s.trim()); - const resolved = mcPlus1[index] || csvData.mc0 || ''; - logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, mcPlus1: ${mcPlus1.length} items)`, 'DEBUG'); - return resolved; - } - - // Variables avec index T+1_X - if (cleanVar.startsWith('T+1_')) { - const index = parseInt(cleanVar.split('_')[1]) - 1; - const tPlus1 = (csvData.tPlus1 || '').split(',').map(s => s.trim()); - const resolved = tPlus1[index] || csvData.t0 || ''; - logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, tPlus1: ${tPlus1.length} items)`, 'DEBUG'); - return resolved; - } - - // Variables avec index L+1_X - if (cleanVar.startsWith('L+1_')) { - const index = parseInt(cleanVar.split('_')[1]) - 1; - const lPlus1 = (csvData.lPlus1 || '').split(',').map(s => s.trim()); - const resolved = lPlus1[index] || ''; - logSh(` 🔍 Variable ${cleanVar} → "${resolved}" (index ${index}, lPlus1: ${lPlus1.length} items)`, 'DEBUG'); - return resolved; - } - - // Variable inconnue - logSh(` ⚠️ Variable inconnue: "${cleanVar}" (match: "${match}")`, 'WARNING'); - return csvData.mc0 || ''; - }); - }; - - // Nettoyer l'instruction des balises HTML et résoudre les variables - const originalInstruction = instruction; - - // NE PLUS nettoyer le HTML ici - c'est fait dans ElementExtraction.js - instruction = instruction.trim(); - - logSh(` 📝 Instruction avant résolution (${tag}): ${instruction.substring(0, 100)}...`, 'DEBUG'); - instruction = resolveVariables(instruction, csvData); - logSh(` ✅ Instruction après résolution (${tag}): ${instruction.substring(0, 100)}...`, 'DEBUG'); - - // Nettoyer les accolades mal formées restantes - instruction = instruction - .replace(/\{[^}]*/g, '') // Supprimer accolades non fermées - .replace(/[{}]/g, '') // Supprimer accolades isolées - .trim(); - - // Vérifier que l'instruction n'est pas vide ou invalide - if (!instruction || instruction.length < 10) { - logSh(`⚠️ Instruction trop courte ou vide pour ${tag}, skip`, 'WARNING'); - continue; - } - - const prompt = `Tu es un expert en rédaction SEO. Tu dois générer du contenu professionnel et naturel. - -CONTEXTE: -- Mot-clé principal: ${csvData.mc0} -- Titre principal: ${csvData.t0} -- Personnalité: ${csvData.personality?.nom} (${csvData.personality?.style}) - -INSTRUCTION SPÉCIFIQUE: -${instruction} - -CONSIGNES: -- Contenu naturel et engageant -- Intégration naturelle du mot-clé "${csvData.mc0}" -- Style ${csvData.personality?.style || 'professionnel'} -- Pas de formatage markdown -- Réponse directe sans préambule - -RÉPONSE:`; - - const response = await LLMManager.callLLM(llmProvider, prompt, { - temperature: 0.9, - maxTokens: 300, - timeout: 30000 - }, csvData.personality); - - if (response && response.trim()) { - result.content[tag] = cleanGeneratedContent(response.trim()); - result.stats.processed++; - result.stats.enhanced++; - } else { - logSh(`⚠️ Réponse vide pour ${tag}`, 'WARNING'); - result.content[tag] = `Contenu ${tag} généré automatiquement`; - } - - } catch (error) { - logSh(`❌ Erreur génération ${tag}: ${error.message}`, 'ERROR'); - result.content[tag] = `Contenu ${tag} - Erreur de génération`; + if (batch.length > 0) { + batches.push(...batch); } } - + + logSh(`📊 Total éléments à générer: ${batches.length}`, 'INFO'); + + // 🔥 NOUVEAU : Tracker le dernier titre généré pour l'associer au texte suivant + let lastGeneratedTitle = null; + + // Générer chaque élément avec prompt typé + for (let i = 0; i < batches.length; i++) { + const { tag, item, isCouple } = batches[i]; + + try { + logSh(`🎯 Génération: ${tag}${isCouple ? ` (couple: ${isCouple})` : ''}`, 'DEBUG'); + + // 🔥 NOUVEAU : Détecter si le prochain élément est un texte associé à un titre + const isTitle = isCouple === 'titre'; + const nextBatch = i < batches.length - 1 ? batches[i + 1] : null; + const nextIsText = nextBatch && (nextBatch.isCouple === 'texte'); + + if (isTitle && nextIsText) { + logSh(` 🔗 Détecté couple titre→texte : ${tag} → ${nextBatch.tag}`, 'DEBUG'); + } + + // Extraire et résoudre l'instruction + let instruction = extractInstruction(tag, item); + instruction = instruction.trim(); + instruction = resolveVariables(instruction, csvData); + + // Résoudre variables non résolues manuellement + const unresolvedPattern = /\b(MC\+1_\d+|T\+1_\d+|L\+1_\d+|MC0|T0|T-1|L-1)\b/gi; + const unresolved = instruction.match(unresolvedPattern); + if (unresolved) { + unresolved.forEach(varName => { + const upperVar = varName.toUpperCase(); + let replacement = csvData.mc0 || ''; + + if (upperVar === 'MC0') replacement = csvData.mc0 || ''; + else if (upperVar === 'T0') replacement = csvData.t0 || ''; + else if (upperVar === 'T-1') replacement = csvData.tMinus1 || ''; + else if (upperVar === 'L-1') replacement = csvData.lMinus1 || ''; + else if (upperVar.startsWith('MC+1_')) { + const idx = parseInt(upperVar.split('_')[1]) - 1; + replacement = (csvData.mcPlus1 || '').split(',')[idx]?.trim() || csvData.mc0 || ''; + } else if (upperVar.startsWith('T+1_')) { + const idx = parseInt(upperVar.split('_')[1]) - 1; + replacement = (csvData.tPlus1 || '').split(',')[idx]?.trim() || csvData.t0 || ''; + } else if (upperVar.startsWith('L+1_')) { + const idx = parseInt(upperVar.split('_')[1]) - 1; + replacement = (csvData.lPlus1 || '').split(',')[idx]?.trim() || ''; + } + + instruction = instruction.replace(new RegExp(varName, 'gi'), replacement); + }); + } + + // Nettoyer accolades mal formées + instruction = instruction.replace(/\{[^}]*/g, '').replace(/[{}]/g, '').trim(); + + if (!instruction || instruction.length < 10) { + logSh(` ⚠️ ${tag}: Instruction trop courte (${instruction?.length || 0} chars), utilisation fallback`, 'WARNING'); + instruction = `Générer du contenu pertinent pour ${tag} sur "${csvData.mc0}"`; + } + + // Détecter le type d'élément + const elementType = detectElementType(tag); + logSh(` 📝 Type détecté: ${elementType}`, 'DEBUG'); + + // 🔥 NOUVEAU : Si c'est un texte et qu'on a un titre généré juste avant, l'utiliser + const shouldUseTitle = (isCouple === 'texte') && lastGeneratedTitle; + if (shouldUseTitle) { + logSh(` 🎯 Utilisation du titre associé: "${lastGeneratedTitle}"`, 'INFO'); + } + + // Créer le prompt avec contraintes de longueur + titre associé si disponible + const prompt = createTypedPrompt(tag, elementType, instruction, csvData, shouldUseTitle ? lastGeneratedTitle : null); + + // Appeler le LLM avec maxTokens augmenté + let maxTokens = 1000; // Défaut augmenté + if (llmProvider.startsWith('gpt-5')) { + maxTokens = 2500; // GPT-5 avec reasoning tokens + } else if (llmProvider.startsWith('gpt-4')) { + maxTokens = 1500; + } else if (llmProvider.startsWith('claude')) { + maxTokens = 2000; + } + + logSh(` 📏 MaxTokens: ${maxTokens} pour ${llmProvider}`, 'DEBUG'); + + let response; + try { + response = await LLMManager.callLLM(llmProvider, prompt, { + temperature: 0.9, + maxTokens: maxTokens, + timeout: 45000 + }, csvData.personality); + } catch (llmError) { + logSh(`❌ Erreur LLM pour ${tag}: ${llmError.message}`, 'ERROR'); + response = null; + } + + if (response && response.trim()) { + const cleaned = cleanGeneratedContent(response.trim()); + result.content[tag] = cleaned; + result.stats.processed++; + result.stats.enhanced++; + + // 🔥 NOUVEAU : Si c'est un titre, le stocker pour l'utiliser avec le texte suivant + if (isTitle) { + lastGeneratedTitle = cleaned; + logSh(` 📌 Titre stocké pour le texte suivant: "${cleaned}"`, 'DEBUG'); + } + + // 🔥 NOUVEAU : Si on vient de générer un texte, réinitialiser le titre + if (isCouple === 'texte') { + lastGeneratedTitle = null; + } + + const wordCount = cleaned.split(/\s+/).length; + logSh(` ✅ Généré: ${tag} (${wordCount} mots)`, 'DEBUG'); + } else { + // Fallback avec prompt simplifié + logSh(` ⚠️ Réponse vide, retry avec gpt-4o-mini`, 'WARNING'); + + try { + const simplePrompt = `Rédige du contenu professionnel sur "${csvData.mc0}" pour ${tag}. ${elementType === 'titre' ? 'Maximum 15 mots.' : elementType === 'intro' || elementType === 'conclusion' ? 'Environ 50-80 mots.' : 'Environ 100-150 mots.'}`; + + const retryResponse = await LLMManager.callLLM('gpt-4o-mini', simplePrompt, { + temperature: 0.7, + maxTokens: 500, + timeout: 20000 + }); + + if (retryResponse && retryResponse.trim()) { + const cleaned = cleanGeneratedContent(retryResponse.trim()); + result.content[tag] = cleaned; + result.stats.processed++; + result.stats.enhanced++; + + // 🔥 NOUVEAU : Stocker le titre même dans le fallback + if (isTitle) { + lastGeneratedTitle = cleaned; + logSh(` 📌 Titre stocké (fallback): "${cleaned}"`, 'DEBUG'); + } + if (isCouple === 'texte') { + lastGeneratedTitle = null; + } + + logSh(` ✅ Retry réussi pour ${tag}`, 'INFO'); + } else { + result.content[tag] = `Contenu professionnel sur ${csvData.mc0}. [Généré automatiquement]`; + result.stats.processed++; + + // 🔥 NOUVEAU : Réinitialiser si c'était un texte + if (isCouple === 'texte') { + lastGeneratedTitle = null; + } + } + } catch (retryError) { + result.content[tag] = `Contenu professionnel sur ${csvData.mc0}. [Erreur: ${retryError.message.substring(0, 50)}]`; + result.stats.processed++; + + // 🔥 NOUVEAU : Réinitialiser si c'était un texte + if (isCouple === 'texte') { + lastGeneratedTitle = null; + } + + logSh(` ❌ Retry échoué: ${retryError.message}`, 'ERROR'); + } + } + + } catch (error) { + logSh(`❌ Erreur génération ${tag}: ${error.message}`, 'ERROR'); + result.content[tag] = `Contenu professionnel sur ${csvData.mc0}. [Erreur: ${error.message.substring(0, 50)}]`; + result.stats.processed++; + + // 🔥 NOUVEAU : Réinitialiser si c'était un texte + if (isCouple === 'texte') { + lastGeneratedTitle = null; + } + } + } + result.stats.duration = Date.now() - startTime; - - logSh(`✅ Génération simple terminée: ${result.stats.enhanced}/${result.stats.processed} éléments (${result.stats.duration}ms)`, 'INFO'); - + + const generatedElements = Object.keys(result.content).length; + const successfulElements = result.stats.enhanced; + const fallbackElements = generatedElements - successfulElements; + + logSh(`✅ Génération terminée: ${generatedElements} éléments`, 'INFO'); + logSh(` ✓ Succès: ${successfulElements}, Fallback: ${fallbackElements}, Durée: ${result.stats.duration}ms`, 'INFO'); + return result; - + } catch (error) { result.stats.duration = Date.now() - startTime; logSh(`❌ Échec génération simple: ${error.message}`, 'ERROR');