diff --git a/lib/MissingKeywords.js b/lib/MissingKeywords.js index 7a5ec0b..cda10a5 100644 --- a/lib/MissingKeywords.js +++ b/lib/MissingKeywords.js @@ -5,6 +5,68 @@ const { logSh } = require('./ErrorReporting'); const { callLLM } = require('./LLMManager'); +const { logElementsList } = require('./selective-enhancement/SelectiveUtils'); + +// ============================================ +// OPTION B: Détection et génération multi-variables +// Ajout conservatif - ne remplace pas l'existant +// ============================================ + +/** + * Extraire les variables individuelles d'un resolvedContent + * Ex: "[T+1_5 non défini][MC+1_5 non défini]" → ["T+1_5", "MC+1_5"] + */ +function extractIndividualPlaceholders(resolvedContent) { + if (!resolvedContent || typeof resolvedContent !== 'string') { + return []; + } + + const regex = /\[([A-Z]+[+-]\d+_\d+) non (défini|résolu)\]/g; + const placeholders = []; + let match; + + while ((match = regex.exec(resolvedContent)) !== null) { + const varName = match[1]; + if (!placeholders.includes(varName)) { + placeholders.push(varName); + } + } + + return placeholders; +} + +/** + * Déterminer le type d'une variable (pour contexte de génération) + */ +function getVariableType(varName) { + if (varName.startsWith('T+') || varName.startsWith('T-')) { + return 'titre'; // Titre court + } else if (varName.startsWith('MC+') || varName.startsWith('MC-')) { + return 'mot-clé'; // Mot-clé SEO + } else if (varName.startsWith('L+') || varName.startsWith('L-')) { + return 'lien'; // Slug URL + } + return 'texte'; // Fallback +} + +/** + * Analyser quels éléments ont plusieurs placeholders + * Retourne: { elementName: [variables], ... } + */ +function analyzeMultiPlaceholderElements(missingElements) { + const multiVarElements = {}; + + missingElements.forEach(element => { + const placeholders = extractIndividualPlaceholders(element.currentContent); + + if (placeholders.length > 1) { + multiVarElements[element.name] = placeholders; + logSh(` 🔍 Élément multi-variables détecté: [${element.name}] a ${placeholders.length} variables: ${placeholders.join(', ')}`, 'DEBUG'); + } + }); + + return multiVarElements; +} /** * Génère automatiquement les mots-clés manquants pour les éléments non définis @@ -14,14 +76,21 @@ const { callLLM } = require('./LLMManager'); */ async function generateMissingKeywords(elements, csvData) { logSh('>>> GÉNÉRATION MOTS-CLÉS MANQUANTS <<<', 'INFO'); - + // 1. IDENTIFIER tous les éléments manquants const missingElements = []; elements.forEach(element => { - if (element.resolvedContent.includes('non défini') || - element.resolvedContent.includes('non résolu') || - element.resolvedContent.trim() === '') { - + // Détecter les patterns de contenu manquant + const isNonDefini = element.resolvedContent.includes('non défini'); + const isNonResolu = element.resolvedContent.includes('non résolu'); + const isEmpty = element.resolvedContent.trim() === ''; + + // Détecter aussi les patterns comme [MC+1_5 non défini], [T+1_3 non défini], etc. + const hasPlaceholder = /\[(MC|T|L)[+-]\d+_\d+ non défini\]/i.test(element.resolvedContent); + + if (isNonDefini || isNonResolu || isEmpty || hasPlaceholder) { + logSh(`🔍 Élément manquant détecté: [${element.name}] = "${element.resolvedContent}"`, 'DEBUG'); + missingElements.push({ tag: element.originalTag, name: element.name, @@ -31,24 +100,106 @@ async function generateMissingKeywords(elements, csvData) { }); } }); - + if (missingElements.length === 0) { - logSh('Aucun mot-clé manquant détecté', 'INFO'); - return {}; + logSh('✅ Aucun mot-clé manquant détecté', 'INFO'); + return elements; // Retourner les éléments inchangés } - - logSh(`${missingElements.length} mots-clés manquants détectés`, 'INFO'); + + logSh(`⚠️ ${missingElements.length} mots-clés manquants détectés:`, 'INFO'); + missingElements.forEach((missing, index) => { + logSh(` ${index + 1}. [${missing.name}] = "${missing.currentContent}"`, 'INFO'); + }); // 2. ANALYSER le contexte global disponible const contextAnalysis = analyzeAvailableContext(elements, csvData); // 3. GÉNÉRER tous les manquants en UN SEUL appel IA - const generatedKeywords = await callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData); - + let generatedKeywords = await callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData); + + // ✅ 3.5. RETRY pour éléments encore manquants + if (generatedKeywords._missingElements && generatedKeywords._missingElements.length > 0) { + const stillMissingNames = generatedKeywords._missingElements; + logSh(`🔄 RETRY: Génération ciblée pour ${stillMissingNames.length} éléments manquants`, 'INFO'); + + // Filtrer les éléments encore manquants + const retryElements = missingElements.filter(e => stillMissingNames.includes(e.name)); + + try { + // Retry avec un prompt simplifié + const retryKeywords = await callOpenAIForMissingKeywords(retryElements, contextAnalysis, csvData); + + // Merger les résultats du retry avec les résultats existants + Object.keys(retryKeywords).forEach(key => { + if (key !== '_missingElements' && key !== '_subVariables') { + generatedKeywords[key] = retryKeywords[key]; + } + }); + + logSh(`✅ RETRY réussi: ${Object.keys(retryKeywords).length - (retryKeywords._subVariables ? 1 : 0)} éléments supplémentaires générés`, 'INFO'); + } catch (retryError) { + logSh(`⚠️ RETRY échoué: ${retryError.message}`, 'WARN'); + logSh(`⚠️ Le workflow continuera avec les éléments disponibles`, 'WARN'); + } + } + // 4. METTRE À JOUR les éléments avec les nouveaux mots-clés const updatedElements = updateElementsWithKeywords(elements, generatedKeywords); - - logSh(`Mots-clés manquants générés: ${Object.keys(generatedKeywords).length}`, 'INFO'); + + logSh(`✅ Mots-clés manquants générés: ${Object.keys(generatedKeywords).length}`, 'INFO'); + + // 📊 LOG COMPLET: Afficher TOUS les mots-clés (Google Sheets + Générés) + logSh(`\n📋 RÉCAPITULATIF COMPLET DES MOTS-CLÉS AVANT GÉNÉRATION:`, 'INFO'); + + // 1. Mots-clés récupérés depuis Google Sheets + logSh(`\n🔹 MOTS-CLÉS GOOGLE SHEETS:`, 'INFO'); + logSh(` MC0 (principal): "${csvData.mc0 || 'VIDE'}"`, 'INFO'); + logSh(` T0 (titre principal): "${csvData.t0 || 'VIDE'}"`, 'INFO'); + logSh(` T-1: "${csvData.tMinus1 || 'VIDE'}"`, 'INFO'); + logSh(` L-1: "${csvData.lMinus1 || 'VIDE'}"`, 'INFO'); + + if (csvData.mcPlus1) { + const mcPlus1Array = csvData.mcPlus1.split(',').map(s => s.trim()); + logSh(` MC+1 (${mcPlus1Array.length} valeurs):`, 'INFO'); + mcPlus1Array.forEach((val, idx) => { + logSh(` [${idx + 1}] "${val}"`, 'INFO'); + }); + } + + if (csvData.tPlus1) { + const tPlus1Array = csvData.tPlus1.split(',').map(s => s.trim()); + logSh(` T+1 (${tPlus1Array.length} valeurs):`, 'INFO'); + tPlus1Array.forEach((val, idx) => { + logSh(` [${idx + 1}] "${val}"`, 'INFO'); + }); + } + + if (csvData.lPlus1) { + const lPlus1Array = csvData.lPlus1.split(',').map(s => s.trim()); + logSh(` L+1 (${lPlus1Array.length} valeurs):`, 'INFO'); + lPlus1Array.forEach((val, idx) => { + logSh(` [${idx + 1}] "${val}"`, 'INFO'); + }); + } + + // 2. Mots-clés générés par l'IA + logSh(`\n🔹 MOTS-CLÉS GÉNÉRÉS PAR IA:`, 'INFO'); + const subVariables = generatedKeywords._subVariables || {}; + Object.keys(generatedKeywords).forEach(key => { + if (key === '_subVariables' || key === '_missingElements') return; + logSh(` [${key}]: "${generatedKeywords[key]}"`, 'INFO'); + }); + + if (Object.keys(subVariables).length > 0) { + logSh(`\n 📌 Sous-variables (${Object.keys(subVariables).length}):`, 'INFO'); + Object.keys(subVariables).forEach(key => { + logSh(` [${key}]: "${subVariables[key]}"`, 'INFO'); + }); + } + + // 📊 LOG COMPLET: Afficher TOUS les éléments combinés (Google Sheets + IA) + logElementsList(updatedElements, 'LISTE COMPLÈTE COMBINÉE (Google Sheets + IA)', generatedKeywords); + return updatedElements; } @@ -61,14 +212,28 @@ async function generateMissingKeywords(elements, csvData) { function analyzeAvailableContext(elements, csvData) { const availableKeywords = []; const availableContent = []; - - // Récupérer tous les mots-clés/contenu déjà disponibles + + // ✅ NOUVEAU : Inclure les mots-clés de csvData (incluant ceux générés à l'étape 0) + if (csvData.mcPlus1) { + availableKeywords.push(...csvData.mcPlus1.split(',').map(s => s.trim()).filter(s => s)); + } + if (csvData.tPlus1) { + availableKeywords.push(...csvData.tPlus1.split(',').map(s => s.trim()).filter(s => s)); + } + if (csvData.lPlus1) { + availableKeywords.push(...csvData.lPlus1.split(',').map(s => s.trim()).filter(s => s)); + } + if (csvData.tMinus1) { + availableKeywords.push(csvData.tMinus1); + } + + // Récupérer tous les mots-clés/contenu déjà disponibles depuis les éléments elements.forEach(element => { - if (element.resolvedContent && - !element.resolvedContent.includes('non défini') && + if (element.resolvedContent && + !element.resolvedContent.includes('non défini') && !element.resolvedContent.includes('non résolu') && element.resolvedContent.trim() !== '') { - + if (element.type.includes('titre')) { availableKeywords.push(element.resolvedContent); } else { @@ -76,14 +241,13 @@ function analyzeAvailableContext(elements, csvData) { } } }); - + return { mainKeyword: csvData.mc0, mainTitle: csvData.t0, - availableKeywords: availableKeywords, + availableKeywords: availableKeywords, // ✅ Maintenant inclut csvData + elements availableContent: availableContent, - theme: csvData.mc0, // Thème principal - businessContext: "Autocollant.fr - signalétique personnalisée, plaques" + theme: csvData.mc0 // Thème principal }; } @@ -100,25 +264,115 @@ function getElementContext(element, allElements, csvData) { hierarchyLevel: element.name, nearbyElements: [] }; - + // Trouver les éléments proches dans la hiérarchie const elementParts = element.name.split('_'); if (elementParts.length >= 2) { - const baseLevel = elementParts.slice(0, 2).join('_'); // Ex: "Titre_H3" - + const baseLevel = elementParts.slice(0, 2).join('_'); // Ex: "Titre_H2", "Txt_H2" + + // Récupérer TOUS les éléments du même type (même baseLevel) qui sont déjà résolus allElements.forEach(otherElement => { - if (otherElement.name.startsWith(baseLevel) && - otherElement.resolvedContent && - !otherElement.resolvedContent.includes('non défini')) { - + // Vérifier que l'élément est du même type et déjà résolu + if (otherElement.name !== element.name && // Pas l'élément lui-même + otherElement.name.startsWith(baseLevel) && + otherElement.resolvedContent && + !otherElement.resolvedContent.includes('non défini') && + !otherElement.resolvedContent.includes('non résolu') && + otherElement.resolvedContent.trim() !== '') { + context.nearbyElements.push(otherElement.resolvedContent); } }); } - + return context; } +/** + * Construire le contexte hiérarchique compact pour un élément + * @param {Object} missing - Élément manquant + * @param {Array} allMissingElements - Tous les éléments manquants + * @param {Object} csvData - Données CSV + * @returns {string} Contexte compact (ex: "H2 existants: 'Titre1', 'Titre2'") + */ +function buildHierarchicalContext(missing, allMissingElements, csvData) { + const elementName = missing.name; + const parts = elementName.split('_'); + + // Récupérer le contexte depuis missing.context.nearbyElements (déjà calculé par getElementContext) + const nearbyElements = missing.context?.nearbyElements || []; + + // Déterminer le type d'élément + if (elementName.startsWith('Titre_H2')) { + // Pour un titre H2 : afficher les autres titres H2 existants + const existingH2 = nearbyElements.filter(el => el && el.length > 0).slice(0, 3); + if (existingH2.length > 0) { + const shortened = existingH2.map(t => t.length > 30 ? t.substring(0, 30) + '...' : t); + return `H2 existants: ${shortened.map(t => `"${t}"`).join(', ')}`; + } + return 'Premier H2 du document'; + + } else if (elementName.startsWith('Titre_H3')) { + // Pour un titre H3 : afficher les autres H3 + parent H2 + const existingH3 = nearbyElements.filter(el => el && el.length > 0).slice(0, 2); + if (existingH3.length > 0) { + const shortened = existingH3.map(t => t.length > 30 ? t.substring(0, 30) + '...' : t); + return `H3 existants: ${shortened.map(t => `"${t}"`).join(', ')}`; + } + return 'Premier H3 dans cette section'; + + } else if (elementName.startsWith('Txt_')) { + // Pour un texte : afficher MC associé + titre parent + autres textes + const level = elementName.match(/Txt_(H\d+)_(\d+)/); + if (level) { + const hierarchyLevel = level[1]; // H2, H3, etc. + const index = parseInt(level[2]); // 6, 7, etc. + + // Chercher le MC correspondant (MC+1_6 pour Txt_H2_6) + const mcKey = `MC+1_${index}`; + let mcValue = null; + if (csvData.mcPlus1) { + const mcArray = csvData.mcPlus1.split(',').map(s => s.trim()); + if (mcArray[index - 1]) { + mcValue = mcArray[index - 1]; + } + } + + // Chercher le titre parent + const titleName = `Titre_${hierarchyLevel}_${index}`; + const parentTitle = nearbyElements.find(el => el.includes(titleName)) || nearbyElements[0]; + + let context = ''; + if (mcValue) { + const shortMc = mcValue.length > 25 ? mcValue.substring(0, 25) + '...' : mcValue; + context = `MC: "${shortMc}"`; + } + if (parentTitle) { + const shortTitle = parentTitle.length > 25 ? parentTitle.substring(0, 25) + '...' : parentTitle; + context += (context ? ' | ' : '') + `Parent: "${shortTitle}"`; + } + if (nearbyElements.length > 1) { + context += (context ? ' | ' : '') + `${nearbyElements.length - 1} autres textes`; + } + return context || 'Premier texte'; + } + + } else if (elementName.startsWith('Faq_')) { + // Pour une FAQ : compter les FAQ existantes + const faqCount = nearbyElements.filter(el => el && el.length > 0).length; + if (faqCount > 0) { + return `${faqCount} FAQ existantes`; + } + return 'Première FAQ'; + } + + // Fallback : afficher le nombre d'éléments proches + if (nearbyElements.length > 0) { + return `${nearbyElements.length} éléments similaires existants`; + } + return ''; +} + /** * Appel IA pour générer tous les mots-clés manquants en un seul batch * @param {Array} missingElements - Éléments manquants @@ -128,52 +382,110 @@ function getElementContext(element, allElements, csvData) { */ async function callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData) { const personality = csvData.personality; - + + // ✅ OPTION B: Détecter les éléments avec plusieurs variables + const multiVarElements = analyzeMultiPlaceholderElements(missingElements); + const hasMultiVarElements = Object.keys(multiVarElements).length > 0; + + if (hasMultiVarElements) { + logSh(`🔍 ${Object.keys(multiVarElements).length} éléments avec variables multiples détectés`, 'INFO'); + } + let prompt = `Tu es ${personality.nom} (${personality.description}). Style: ${personality.style} MISSION: GÉNÈRE ${missingElements.length} MOTS-CLÉS/EXPRESSIONS MANQUANTS pour ${contextAnalysis.mainKeyword} CONTEXTE: -- Sujet: ${contextAnalysis.mainKeyword} -- Entreprise: Autocollant.fr (signalétique) +- Sujet principal: ${contextAnalysis.mainKeyword} - Mots-clés existants: ${contextAnalysis.availableKeywords.slice(0, 3).join(', ')} +🎯 IMPÉRATIF DIVERSITÉ ET SPÉCIFICITÉ: +- VARIE les angles: technique, usage, avantages, comparaisons, stratégies, communauté, équipement +- ÉVITE les répétitions "${contextAnalysis.mainKeyword} X, ${contextAnalysis.mainKeyword} Y, ${contextAnalysis.mainKeyword} Z" +- EXPLORE différents aspects selon le domaine du sujet +- Exemples d'angles variés: + * Produits physiques: matériaux, formats, finitions, usages, durabilité + * Jeux vidéo: stratégies, maps, armes, personnages, équipes, tournois, guides + * Services: méthodes, outils, processus, résultats, comparaisons +- Sois SPÉCIFIQUE, pas générique + ÉLÉMENTS MANQUANTS: `; missingElements.forEach((missing, index) => { - prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`; + const placeholders = extractIndividualPlaceholders(missing.currentContent); + + // Construire le contexte hiérarchique compact + const hierarchyContext = buildHierarchicalContext(missing, missingElements, csvData); + + // ✅ OPTION B: Si l'élément a plusieurs variables, détailler chacune + if (placeholders.length > 1) { + prompt += `${index + 1}. [${missing.name}] a ${placeholders.length} variables distinctes:\n`; + placeholders.forEach(varName => { + const varType = getVariableType(varName); + prompt += ` - [${missing.name}_${varName}] (type: ${varType}) → Génère un ${varType}\n`; + }); + } else { + // Format compact avec hiérarchie + const elementType = missing.type || 'élément'; + if (hierarchyContext) { + prompt += `${index + 1}. [${missing.name}] (${elementType}) — ${hierarchyContext}\n`; + } else { + prompt += `${index + 1}. [${missing.name}] (${elementType})\n`; + } + } }); - + prompt += `\nCONSIGNES: -- Thème: ${contextAnalysis.mainKeyword} -- Mots-clés SEO naturels -- Varie les termes -- Évite répétitions +- Thème principal: ${contextAnalysis.mainKeyword} +- Génère des mots-clés/expressions SEO naturels et VARIÉS +- 🎯 DIVERSITÉ MAXIMALE: Chaque mot-clé doit explorer un angle DIFFÉRENT +- ÉVITE absolument les patterns répétitifs comme "${contextAnalysis.mainKeyword} + adjectif" +- Si un élément a plusieurs variables, génère des valeurs COMPLÈTEMENT DIFFÉRENTES +- Pour les liens (type: lien): slugs URL courts (ex: /guide-debutant, /meilleurs-conseils) +- Pour les titres (type: titre): courts et accrocheurs (5-10 mots max) +- Pour les mots-clés (type: mot-clé): expressions SEO spécifiques et variées -FORMAT: -[${missingElements[0].name}] -mot-clé +EXEMPLES DE DIVERSITÉ ATTENDUE: +- ❌ MAL: "Plaque dibond", "Plaque aluminium", "Plaque personnalisée" +- ✅ BIEN: "Dibond haute résistance", "Finitions gravure laser", "Sur-mesure extérieur" -[${missingElements[1] ? missingElements[1].name : 'exemple'}] -mot-clé +⚠️ CRITIQUE: Tu DOIS générer TOUS les ${missingElements.length} éléments listés ci-dessus. +Ne saute aucun élément, même si tu génères des sous-variables pour certains. -etc...`; +FORMAT DE RÉPONSE EXACT (RESPECTE LES CROCHETS):`; + + // ✅ Donner 3 exemples concrets de la vraie liste + const exampleCount = Math.min(3, missingElements.length); + for (let i = 0; i < exampleCount; i++) { + const example = missingElements[i]; + prompt += ` +[${example.name}] +${example.type.includes('titre') ? 'Titre accrocheur 5-10 mots' : example.type.includes('question') ? 'Question naturelle?' : example.type.includes('reponse') ? 'Réponse claire et concise' : 'Expression SEO spécifique'} +`; + } + + prompt += ` +... Continue avec TOUS les ${missingElements.length} éléments de la liste ci-dessus ... + +⚠️ IMPORTANT: Utilise EXACTEMENT les noms entre crochets [Titre_H2_3], [Txt_H2_6], etc. Ne change RIEN aux noms!`; try { - logSh('Génération mots-clés manquants...', 'DEBUG'); - - // Utilisation du LLM Manager avec fallback - const response = await callLLM('openai', prompt, { + logSh('🤖 Appel LLM pour génération mots-clés manquants...', 'INFO'); + + // Utilisation du LLM Manager avec fallback (le prompt/réponse seront loggés par LLMManager) + const response = await callLLM('gpt-4o-mini', prompt, { temperature: 0.7, - maxTokens: 2000 + maxTokens: 3000 // ✅ Augmenté de 2000 à 3000 pour éviter les coupures }, personality); - + // Parser la réponse const generatedKeywords = parseMissingKeywordsResponse(response, missingElements); - + + logSh(`✅ ${Object.keys(generatedKeywords).length} mots-clés générés avec succès`, 'INFO'); + return generatedKeywords; - + } catch (error) { logSh(`❌ FATAL: Génération mots-clés manquants échouée: ${error}`, 'ERROR'); throw new Error(`FATAL: Génération mots-clés LLM impossible - arrêt du workflow: ${error}`); @@ -187,46 +499,172 @@ etc...`; * @returns {Object} Mots-clés parsés */ function parseMissingKeywordsResponse(response, missingElements) { + logSh('🔍 Début du parsing de la réponse LLM...', 'DEBUG'); + const results = {}; - + const subVariableResults = {}; // ✅ OPTION B: Stocker les sous-variables séparément + const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs; let match; - + while ((match = regex.exec(response)) !== null) { const elementName = match[1].trim(); - const generatedKeyword = match[2].trim(); - - results[elementName] = generatedKeyword; - - logSh(`✓ Mot-clé généré [${elementName}]: "${generatedKeyword}"`, 'DEBUG'); - } - - // VALIDATION: Vérifier qu'on a au moins récupéré des résultats (tolérer doublons) - const uniqueNames = [...new Set(missingElements.map(e => e.name))]; - const parsedCount = Object.keys(results).length; + let generatedKeyword = match[2].trim(); - if (parsedCount === 0) { - logSh(`❌ FATAL: Aucun mot-clé parsé`, 'ERROR'); + // ✅ Nettoyer les commentaires markdown et lignes parasites + // Supprimer les commentaires qui commencent par # + generatedKeyword = generatedKeyword + .split('\n') + .filter(line => !line.trim().startsWith('#')) // Supprimer lignes commençant par # + .filter(line => line.trim() !== '') // Supprimer lignes vides + .join('\n') + .trim(); + + // Ignorer si vide après nettoyage + if (!generatedKeyword) { + logSh(` ⚠️ Élément [${elementName}] ignoré (vide après nettoyage)`, 'DEBUG'); + continue; + } + + // ✅ OPTION B: Détecter si c'est une sous-variable (format: Txt_H2_6_T+1_5) + if (elementName.includes('_T+') || elementName.includes('_MC+') || elementName.includes('_L+') || + elementName.includes('_T-') || elementName.includes('_MC-') || elementName.includes('_L-')) { + + // C'est une sous-variable + subVariableResults[elementName] = generatedKeyword; + logSh(` ✓ Sous-variable générée [${elementName}]: "${generatedKeyword}"`, 'INFO'); + } else { + // Comportement normal + results[elementName] = generatedKeyword; + logSh(` ✓ Mot-clé généré [${elementName}]: "${generatedKeyword}"`, 'INFO'); + } + } + + // ✅ OPTION B: Stocker les sous-variables dans results pour y accéder plus tard + if (Object.keys(subVariableResults).length > 0) { + results._subVariables = subVariableResults; + logSh(`📊 ${Object.keys(subVariableResults).length} sous-variables extraites`, 'INFO'); + } + + logSh(`📊 Parsing terminé: ${Object.keys(results).length} mots-clés extraits`, 'INFO'); + + // ✅ OPTION B: Validation améliorée pour gérer les sous-variables + const uniqueNames = [...new Set(missingElements.map(e => e.name))]; + const parsedCount = Object.keys(results).length - (results._subVariables ? 1 : 0); // Exclure _subVariables du compte + const subVarCount = Object.keys(subVariableResults).length; + + logSh(`🔢 Validation: ${parsedCount} éléments principaux, ${subVarCount} sous-variables / ${uniqueNames.length} tags uniques attendus`, 'INFO'); + + // Vérifier qu'on a au moins récupéré QUELQUE CHOSE + if (parsedCount === 0 && subVarCount === 0) { + logSh(`❌ FATAL: Aucun mot-clé parsé de la réponse LLM`, 'ERROR'); + logSh(`Réponse reçue: ${response.substring(0, 500)}...`, 'ERROR'); throw new Error(`FATAL: Parsing mots-clés échoué complètement - arrêt du workflow`); } - // Warning si doublons détectés (mais on continue) - if (missingElements.length > uniqueNames.length) { - const doublonsCount = missingElements.length - uniqueNames.length; - logSh(`⚠️ ${doublonsCount} doublons détectés dans les tags XML (${uniqueNames.length} tags uniques)`, 'WARNING'); + // ✅ OPTION B: Si on a des sous-variables, vérifier qu'on couvre tous les éléments + if (subVarCount > 0) { + // Pour chaque élément unique, vérifier qu'il a soit une valeur principale, soit des sous-variables + const elementsManquants = []; + + uniqueNames.forEach(name => { + const hasMainValue = results[name]; + const hasSubVariables = Object.keys(subVariableResults).some(key => key.startsWith(`${name}_`)); + + if (!hasMainValue && !hasSubVariables) { + elementsManquants.push(name); + } + }); + + if (elementsManquants.length > 0) { + logSh(`⚠️ AVERTISSEMENT: ${elementsManquants.length}/${uniqueNames.length} éléments n'ont ni valeur principale ni sous-variables`, 'WARN'); + logSh(`Éléments manquants:`, 'WARN'); + elementsManquants.forEach(name => logSh(` - [${name}]`, 'WARN')); + + // ✅ Retourner les éléments manquants pour permettre un retry + results._missingElements = elementsManquants; + logSh(`💡 Ces éléments seront réessayés dans un second appel LLM`, 'INFO'); + } + + logSh(`✅ Validation OK: ${parsedCount} éléments + ${subVarCount} sous-variables couvrent ${uniqueNames.length} tags uniques`, 'INFO'); + } else { + // Comportement classique si pas de sous-variables + if (parsedCount < uniqueNames.length) { + const manquants = uniqueNames.length - parsedCount; + logSh(`⚠️ AVERTISSEMENT: Parsing incomplet - ${manquants}/${uniqueNames.length} tags uniques non parsés`, 'WARN'); + + const elementsNonParses = uniqueNames.filter(name => !results[name]); + logSh(`Éléments non parsés:`, 'WARN'); + elementsNonParses.forEach(name => logSh(` - [${name}]`, 'WARN')); + + // ✅ Retourner les éléments manquants pour permettre un retry + results._missingElements = elementsNonParses; + logSh(`💡 Ces éléments seront réessayés dans un second appel LLM`, 'INFO'); + } else { + logSh(`✅ Validation OK: ${parsedCount} mots-clés parsés pour ${uniqueNames.length} tags uniques`, 'INFO'); + } } - // Vérifier qu'on a au moins autant de résultats que de tags uniques - if (parsedCount < uniqueNames.length) { - const manquants = uniqueNames.length - parsedCount; - logSh(`❌ FATAL: Parsing incomplet - ${manquants}/${uniqueNames.length} tags uniques non parsés`, 'ERROR'); - throw new Error(`FATAL: Parsing mots-clés incomplet (${manquants}/${uniqueNames.length} manquants) - arrêt du workflow`); - } - - logSh(`✅ ${parsedCount} mots-clés parsés pour ${uniqueNames.length} tags uniques (${missingElements.length} éléments total)`, 'INFO'); return results; } +/** + * Re-résoudre les instructions en remplaçant tous les placeholders + * SOLUTION B: Fix synchronisation resolvedContent <-> instructions + * @param {string} instructionTemplate - Instructions avec placeholders + * @param {Object} allGeneratedKeywords - Tous les mots-clés générés (tag => keyword) + * @param {Object} element - L'élément courant pour obtenir son resolvedContent + * @returns {string} Instructions avec placeholders remplacés + */ +function reResolveInstructions(instructionTemplate, allGeneratedKeywords, element) { + if (!instructionTemplate || typeof instructionTemplate !== 'string') { + return instructionTemplate; + } + + // ✅ OPTION B: Récupérer les sous-variables si disponibles + const subVariables = allGeneratedKeywords._subVariables || {}; + const hasSubVariables = Object.keys(subVariables).length > 0; + + let resolved = instructionTemplate; + + // ✅ OPTION B: Stratégie améliorée pour multi-variables + resolved = resolved.replace( + /\[([^\]]+) non (défini|résolu)\]/g, + (match, varName) => { + // 1. Chercher d'abord dans les sous-variables spécifiques à cet élément + if (hasSubVariables && element && element.name) { + const subVarKey = `${element.name}_${varName}`; + if (subVariables[subVarKey]) { + logSh(` 🔄 Placeholder résolu via sous-variable: "${match}" → "${subVariables[subVarKey]}"`, 'DEBUG'); + return subVariables[subVarKey]; + } + } + + // 2. Chercher dans les mots-clés générés par nom de variable exact + for (const [tagName, generatedKeyword] of Object.entries(allGeneratedKeywords)) { + if (tagName === '_subVariables' || tagName === '_missingElements') continue; // Skip metadata + + // Match exact du varName dans le tagName + if (tagName.includes(varName)) { + logSh(` 🔄 Placeholder "${match}" résolu via tag exact [${tagName}] → "${generatedKeyword}"`, 'INFO'); + return generatedKeyword; + } + } + + // 3. Fallback: utiliser le resolvedContent global de l'élément (⚠️ peut causer des répétitions) + if (element && element.resolvedContent && !element.resolvedContent.includes('non défini')) { + logSh(` ⚠️ Placeholder "${match}" résolu via resolvedContent fallback → "${element.resolvedContent}"`, 'WARNING'); + return element.resolvedContent; + } + + // 4. Dernier fallback: garder le placeholder + return match; + } + ); + + return resolved; +} + /** * Mettre à jour les éléments avec les nouveaux mots-clés générés * @param {Array} elements - Éléments originaux @@ -234,24 +672,360 @@ function parseMissingKeywordsResponse(response, missingElements) { * @returns {Array} Éléments mis à jour */ function updateElementsWithKeywords(elements, generatedKeywords) { + logSh('🔄 Mise à jour des éléments avec les nouveaux mots-clés...', 'INFO'); + + // 🔍 DEBUG: Afficher les clés disponibles dans generatedKeywords + logSh(`🔍 DEBUG: Clés disponibles dans generatedKeywords:`, 'DEBUG'); + Object.keys(generatedKeywords).forEach(key => { + if (key !== '_subVariables' && key !== '_missingElements') { + logSh(` - "${key}" → "${generatedKeywords[key]}"`, 'DEBUG'); + } + }); + + let updatedCount = 0; + let instructionsUpdatedCount = 0; + const subVariables = generatedKeywords._subVariables || {}; + const updatedElements = elements.map(element => { const newKeyword = generatedKeywords[element.name]; - - if (newKeyword) { - return { - ...element, - resolvedContent: newKeyword - }; + + // 🔍 DEBUG: Log détaillé pour TOUS les éléments manquants + const isManquant = element.resolvedContent.includes('non défini') || + element.resolvedContent.includes('non résolu') || + element.resolvedContent.trim() === ''; + + if (isManquant) { + logSh(` 🔍 DEBUG MANQUANT: [${element.name}]`, 'WARN'); + logSh(` 📄 resolvedContent: "${element.resolvedContent}"`, 'WARN'); + logSh(` 🔑 Cherche dans generatedKeywords["${element.name}"]: ${newKeyword ? `TROUVÉ = "${newKeyword}"` : '❌ PAS TROUVÉ'}`, 'WARN'); + + // Chercher une clé similaire + const similarKeys = Object.keys(generatedKeywords).filter(key => + key !== '_subVariables' && key !== '_missingElements' && + (key.includes(element.name) || element.name.includes(key)) + ); + if (similarKeys.length > 0) { + logSh(` 💡 Clés similaires trouvées: ${similarKeys.join(', ')}`, 'WARN'); + } } - + + // ✅ OPTION B: Si pas de valeur principale mais des sous-variables existent + const hasSubVariables = Object.keys(subVariables).some(key => key.startsWith(`${element.name}_`)); + + if (newKeyword || hasSubVariables) { + updatedCount++; + + // ✅ OPTION B: Si on a des sous-variables, construire une valeur résumée + let finalKeyword = newKeyword; + if (!finalKeyword && hasSubVariables) { + // Combiner les sous-variables en une seule valeur pour resolvedContent + const subVarKeys = Object.keys(subVariables).filter(key => key.startsWith(`${element.name}_`)); + finalKeyword = subVarKeys.map(key => subVariables[key]).join(' | '); + logSh(` ✓ Élément [${element.name}] construit depuis ${subVarKeys.length} sous-variables: "${finalKeyword.substring(0, 80)}..."`, 'DEBUG'); + } else { + logSh(` ✓ Élément [${element.name}] mis à jour: "${element.resolvedContent}" → "${newKeyword}"`, 'DEBUG'); + } + + // ✅ SOLUTION B: Re-résoudre aussi les instructions si elles existent + const updatedElement = { + ...element, + resolvedContent: finalKeyword + }; + + if (element.instructions) { + const beforeInstructions = element.instructions; + updatedElement.instructions = reResolveInstructions( + element.instructions, + generatedKeywords, + updatedElement // ✅ Passer l'élément avec le nouveau resolvedContent + ); + + // Log si les instructions ont changé + if (beforeInstructions !== updatedElement.instructions) { + instructionsUpdatedCount++; + logSh(` ✨ Instructions mises à jour pour [${element.name}]`, 'DEBUG'); + } + } + + return updatedElement; + } + return element; }); - - logSh('Éléments mis à jour avec nouveaux mots-clés', 'INFO'); + + logSh(`✅ ${updatedCount} éléments mis à jour avec nouveaux mots-clés`, 'INFO'); + if (instructionsUpdatedCount > 0) { + logSh(`✅ ${instructionsUpdatedCount} instructions re-résolues avec nouveaux mots-clés`, 'INFO'); + } + return updatedElements; } +// ============================================ +// NOUVELLE FONCTIONNALITÉ: Génération variables Google Sheets manquantes +// Génère les variables (MC+1_5, T+1_6, etc.) AVANT extraction pour avoir csvData complet +// ============================================ + +/** + * Scanner le XML pour trouver toutes les variables demandées + * @param {string} xmlTemplate - Template XML (peut être base64) + * @returns {Object} { MC+1: [indices], T+1: [indices], L+1: [indices] } + */ +function scanXMLForVariables(xmlTemplate) { + // Décoder le base64 si nécessaire + const xmlString = xmlTemplate.startsWith(' { + requestedVariables[key] = Array.from(requestedVariables[key]).sort((a, b) => a - b); + }); + + return requestedVariables; +} + +/** + * Trouver les variables manquantes entre ce qui est demandé et ce qui existe + * @param {Object} requestedVariables - Variables demandées par le XML + * @param {Object} csvData - Données Google Sheets + * @returns {Array} Liste des variables manquantes { varName, index, type } + */ +function findMissingVariables(requestedVariables, csvData) { + const missingVariables = []; + + // Helper pour obtenir le nombre d'éléments existants dans csvData + const getExistingCount = (key) => { + const mapping = { + 'MC+1': 'mcPlus1', + 'T+1': 'tPlus1', + 'L+1': 'lPlus1', + 'MC-1': 'mcMinus1', + 'T-1': 'tMinus1', + 'L-1': 'lMinus1' + }; + + const csvKey = mapping[key]; + if (!csvData[csvKey]) return 0; + + return csvData[csvKey].split(',').map(s => s.trim()).filter(s => s.length > 0).length; + }; + + // Pour chaque type de variable (MC+1, T+1, etc.) + Object.keys(requestedVariables).forEach(key => { + const requestedIndices = requestedVariables[key]; + if (requestedIndices.length === 0) return; + + const existingCount = getExistingCount(key); + const maxRequestedIndex = Math.max(...requestedIndices); + + // Si on demande des indices qui n'existent pas + if (maxRequestedIndex > existingCount) { + for (let i = existingCount + 1; i <= maxRequestedIndex; i++) { + missingVariables.push({ + varName: `${key}_${i}`, + index: i, + type: key, + fullKey: key + }); + } + } + }); + + return missingVariables; +} + +/** + * Mettre à jour csvData avec les mots-clés générés + * @param {Object} csvData - Données CSV originales + * @param {Object} generatedKeywords - Mots-clés générés { varName: value } + * @returns {Object} csvData mis à jour + */ +function updateCsvDataWithKeywords(csvData, generatedKeywords) { + const updatedCsvData = { ...csvData }; + + // Mapping entre types de variables et clés csvData + const mapping = { + 'MC+1': 'mcPlus1', + 'T+1': 'tPlus1', + 'L+1': 'lPlus1', + 'MC-1': 'mcMinus1', + 'T-1': 'tMinus1', + 'L-1': 'lMinus1' + }; + + // Organiser les mots-clés générés par type + const organized = {}; + Object.keys(generatedKeywords).forEach(varName => { + if (varName.startsWith('_')) return; // Skip metadata + + // Parser varName: "MC+1_5" → type="MC+1", index=5 + const match = varName.match(/^(MC|T|L)([+-])1_(\d+)$/i); + if (!match) return; + + const type = `${match[1].toUpperCase()}${match[2]}1`; + const index = parseInt(match[3]); + + if (!organized[type]) { + organized[type] = {}; + } + organized[type][index] = generatedKeywords[varName]; + }); + + // Mettre à jour csvData pour chaque type + Object.keys(organized).forEach(type => { + const csvKey = mapping[type]; + if (!csvKey) return; + + // Récupérer les valeurs existantes + const existing = updatedCsvData[csvKey] + ? updatedCsvData[csvKey].split(',').map(s => s.trim()).filter(s => s.length > 0) + : []; + + // Créer un array complet avec les nouvelles valeurs + const maxIndex = Math.max(...Object.keys(organized[type]).map(i => parseInt(i))); + const complete = []; + + for (let i = 1; i <= maxIndex; i++) { + if (organized[type][i]) { + // Nouvelle valeur générée + complete.push(organized[type][i]); + } else if (existing[i - 1]) { + // Valeur existante + complete.push(existing[i - 1]); + } else { + // Aucune valeur (ne devrait pas arriver) + complete.push(''); + } + } + + // Mettre à jour csvData + updatedCsvData[csvKey] = complete.join(', '); + }); + + return updatedCsvData; +} + +/** + * Fonction principale: Générer les variables Google Sheets manquantes + * @param {string} xmlTemplate - Template XML + * @param {Object} csvData - Données Google Sheets + * @returns {Object} csvData mis à jour avec toutes les variables + */ +async function generateMissingSheetVariables(xmlTemplate, csvData) { + logSh('>>> GÉNÉRATION VARIABLES GOOGLE SHEETS MANQUANTES <<<', 'INFO'); + + // 1. Scanner le XML pour trouver toutes les variables demandées + const requestedVariables = scanXMLForVariables(xmlTemplate); + + logSh('📊 Variables demandées dans le XML:', 'INFO'); + Object.keys(requestedVariables).forEach(key => { + if (requestedVariables[key].length > 0) { + logSh(` ${key}: indices ${requestedVariables[key].join(', ')}`, 'INFO'); + } + }); + + // 2. Comparer avec ce qui existe dans csvData + const missingVariables = findMissingVariables(requestedVariables, csvData); + + if (missingVariables.length === 0) { + logSh('✅ Aucune variable Google Sheets manquante', 'INFO'); + return csvData; + } + + logSh(`⚠️ ${missingVariables.length} variables Google Sheets manquantes:`, 'INFO'); + missingVariables.forEach((missing, index) => { + logSh(` ${index + 1}. ${missing.varName} (type: ${getVariableType(missing.varName)})`, 'INFO'); + }); + + // 3. Transformer en format compatible avec callOpenAIForMissingKeywords + const pseudoElements = missingVariables.map(varInfo => ({ + name: varInfo.varName, + currentContent: `[${varInfo.varName} non défini]`, + type: getVariableType(varInfo.varName), + context: { + mainKeyword: csvData.mc0, + elementType: getVariableType(varInfo.varName), + hierarchyLevel: 'sheet_variable' + } + })); + + // 4. Préparer le contexte avec les mots-clés existants pour éviter les doublons + const existingKeywords = []; + + // Extraire les mots-clés existants de csvData + if (csvData.mcPlus1) { + existingKeywords.push(...csvData.mcPlus1.split(',').map(s => s.trim()).filter(s => s)); + } + if (csvData.tPlus1) { + existingKeywords.push(...csvData.tPlus1.split(',').map(s => s.trim()).filter(s => s)); + } + if (csvData.lPlus1) { + existingKeywords.push(...csvData.lPlus1.split(',').map(s => s.trim()).filter(s => s)); + } + + logSh(`📌 Mots-clés existants à éviter: ${existingKeywords.join(', ')}`, 'DEBUG'); + + // Créer le contexte avec les mots-clés existants + const contextAnalysis = { + mainKeyword: csvData.mc0, + mainTitle: csvData.t0, + availableKeywords: existingKeywords, // ✅ IMPORTANT : éviter les doublons + availableContent: [], + theme: csvData.mc0 + }; + + logSh('🤖 Appel LLM pour génération des variables Google Sheets...', 'INFO'); + const generatedKeywords = await callOpenAIForMissingKeywords( + pseudoElements, + contextAnalysis, + csvData + ); + + // 5. Mettre à jour csvData + const updatedCsvData = updateCsvDataWithKeywords(csvData, generatedKeywords); + + logSh(`✅ csvData mis à jour avec ${Object.keys(generatedKeywords).length - (generatedKeywords._subVariables ? 1 : 0)} nouvelles variables`, 'INFO'); + + // 6. Logger le résultat + logSh('\n📋 RÉCAPITULATIF VARIABLES APRÈS GÉNÉRATION:', 'INFO'); + ['mcPlus1', 'tPlus1', 'lPlus1'].forEach(key => { + if (updatedCsvData[key]) { + const values = updatedCsvData[key].split(',').map(s => s.trim()); + logSh(` ${key}: ${values.length} valeurs`, 'INFO'); + values.forEach((val, idx) => { + logSh(` [${idx + 1}] "${val}"`, 'INFO'); + }); + } + }); + + return updatedCsvData; +} + // Exports CommonJS module.exports = { - generateMissingKeywords + generateMissingKeywords, + generateMissingSheetVariables // ✅ Export de la nouvelle fonction }; \ No newline at end of file diff --git a/lib/pipeline/PipelineExecutor.js b/lib/pipeline/PipelineExecutor.js index 06c27dc..682d59f 100644 --- a/lib/pipeline/PipelineExecutor.js +++ b/lib/pipeline/PipelineExecutor.js @@ -10,7 +10,9 @@ const { tracer } = require('../trace'); const { PipelineDefinition } = require('./PipelineDefinition'); const { getPersonalities, readInstructionsData, selectPersonalityWithAI } = require('../BrainConfig'); const { extractElements, buildSmartHierarchy } = require('../ElementExtraction'); -const { generateMissingKeywords } = require('../MissingKeywords'); +const { generateMissingKeywords, generateMissingSheetVariables } = require('../MissingKeywords'); +const { injectGeneratedContent } = require('../ContentAssembly'); +const { saveGeneratedArticleOrganic } = require('../ArticleStorage'); // Modules d'exécution const { generateSimple } = require('../selective-enhancement/SelectiveUtils'); @@ -31,6 +33,10 @@ class PipelineExecutor { this.currentContent = null; this.executionLog = []; this.checkpoints = []; + this.versionHistory = []; // ✅ Historique des versions sauvegardées + this.parentArticleId = null; // ✅ ID parent pour versioning + this.csvData = null; // ✅ Données CSV pour sauvegarde + this.finalElements = null; // ✅ Éléments extraits pour assemblage this.metadata = { startTime: null, endTime: null, @@ -55,9 +61,12 @@ class PipelineExecutor { this.metadata.startTime = Date.now(); this.executionLog = []; this.checkpoints = []; + this.versionHistory = []; // ✅ Reset version history + this.parentArticleId = null; // ✅ Reset parent ID // Charger les données const csvData = await this.loadData(rowNumber); + this.csvData = csvData; // ✅ Stocker pour sauvegarde // Exécuter les étapes const enabledSteps = pipelineConfig.pipeline.filter(s => s.enabled !== false); @@ -99,6 +108,11 @@ class PipelineExecutor { logSh(`💾 Checkpoint sauvegardé (étape ${step.step})`, 'DEBUG'); } + // ✅ Sauvegarde Google Sheets si activée + if (options.saveIntermediateSteps && this.currentContent) { + await this.saveStepVersion(step, result.modifications || 0, pipelineConfig.name); + } + logSh(`✔ Étape ${step.step} terminée (${stepDuration}ms, ${result.modifications || 0} modifs)`, 'INFO'); } catch (error) { @@ -130,6 +144,7 @@ class PipelineExecutor { finalContent: this.currentContent, executionLog: this.executionLog, checkpoints: this.checkpoints, + versionHistory: this.versionHistory, // ✅ Inclure version history metadata: { ...this.metadata, pipelineName: pipelineConfig.name, @@ -204,12 +219,19 @@ class PipelineExecutor { return { content: this.currentContent, modifications: 0 }; } - // Étape 1: Extraire les éléments depuis le template XML + // 🆕 Étape 0: Générer les variables Google Sheets manquantes (MC+1_5, T+1_6, etc.) + logSh('🔄 Vérification variables Google Sheets...', 'DEBUG'); + const updatedCsvData = await generateMissingSheetVariables(csvData.xmlTemplate, csvData); + // Mettre à jour csvData pour les étapes suivantes + Object.assign(csvData, updatedCsvData); + + // Étape 1: Extraire les éléments depuis le template XML (avec csvData complet) const elements = await extractElements(csvData.xmlTemplate, csvData); logSh(`✓ Extraction: ${elements.length} éléments extraits`, 'DEBUG'); - // Étape 2: Générer les mots-clés manquants + // Étape 2: Générer les mots-clés manquants (titres, textes, FAQ) const finalElements = await generateMissingKeywords(elements, csvData); + this.finalElements = finalElements; // ✅ Stocker pour sauvegarde // Étape 3: Construire la hiérarchie const elementsArray = Array.isArray(finalElements) ? finalElements : @@ -218,7 +240,7 @@ class PipelineExecutor { logSh(`✓ Hiérarchie: ${Object.keys(hierarchy).length} sections`, 'DEBUG'); // Étape 4: Génération simple avec LLM configurable - const llmProvider = step.parameters?.llmProvider || 'claude'; + const llmProvider = step.parameters?.llmProvider || 'claude-sonnet-4-5'; const result = await generateSimple(hierarchy, csvData, { llmProvider }); logSh(`✓ Génération: ${Object.keys(result.content || {}).length} éléments créés avec ${llmProvider}`, 'DEBUG'); @@ -242,7 +264,7 @@ class PipelineExecutor { } // Configuration de la couche - const llmProvider = step.parameters?.llmProvider || 'openai'; + const llmProvider = step.parameters?.llmProvider || 'gpt-4o-mini'; const config = { csvData, personality: csvData.personality, @@ -267,7 +289,7 @@ class PipelineExecutor { return { content: result.content || result, - modifications: result.modificationsCount || 0 + modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); @@ -288,7 +310,7 @@ class PipelineExecutor { return { content: this.currentContent, modifications: 0 }; } - const llmProvider = step.parameters?.llmProvider || 'gemini'; + const llmProvider = step.parameters?.llmProvider || 'gemini-pro'; const config = { csvData, detectorTarget: step.parameters?.detector || 'general', @@ -326,7 +348,7 @@ class PipelineExecutor { return { content: result.content || result, - modifications: result.modificationsCount || 0 + modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, detector: step.parameters?.detector }); @@ -347,7 +369,7 @@ class PipelineExecutor { return { content: this.currentContent, modifications: 0 }; } - const llmProvider = step.parameters?.llmProvider || 'mistral'; + const llmProvider = step.parameters?.llmProvider || 'mistral-small'; const config = { csvData, personality: csvData.personality, @@ -373,7 +395,7 @@ class PipelineExecutor { return { content: result.content || result, - modifications: result.modificationsCount || 0 + modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); @@ -394,7 +416,7 @@ class PipelineExecutor { return { content: this.currentContent, modifications: 0 }; } - const llmProvider = step.parameters?.llmProvider || 'deepseek'; + const llmProvider = step.parameters?.llmProvider || 'deepseek-chat'; const config = { csvData, personality: csvData.personality, @@ -419,7 +441,7 @@ class PipelineExecutor { return { content: result.content || result, - modifications: result.modificationsCount || 0 + modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount }; }, { mode: step.mode, intensity: step.intensity }); @@ -460,6 +482,10 @@ class PipelineExecutor { this.currentContent = null; this.executionLog = []; this.checkpoints = []; + this.versionHistory = []; + this.parentArticleId = null; + this.csvData = null; + this.finalElements = null; this.metadata = { startTime: null, endTime: null, @@ -467,6 +493,67 @@ class PipelineExecutor { personality: null }; } + + /** + * ✅ Sauvegarde une version intermédiaire dans Google Sheets + */ + async saveStepVersion(step, modifications, pipelineName) { + try { + if (!this.csvData || !this.finalElements) { + logSh('⚠️ Données manquantes pour sauvegarde, ignorée', 'WARN'); + return; + } + + // Déterminer la version basée sur le module et le nombre d'étapes + const versionNumber = `v1.${step.step}`; + const stageName = `${step.module}_${step.mode}`; + + logSh(`💾 Sauvegarde ${versionNumber}: ${stageName}`, 'INFO'); + + // Assemblage du contenu + const xmlString = this.csvData.xmlTemplate.startsWith('