// ======================================== // FICHIER: MissingKeywords.js - Version Node.js // Description: Génération automatique des mots-clés manquants // ======================================== 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 * @param {Array} elements - Liste des éléments extraits * @param {Object} csvData - Données CSV avec personnalité * @returns {Object} Éléments mis à jour avec nouveaux mots-clés */ 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 => { // 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, type: element.type, currentContent: element.resolvedContent, context: getElementContext(element, elements, csvData) }); } }); if (missingElements.length === 0) { 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'); 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 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'); // 📊 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; } /** * Analyser le contexte disponible pour guider la génération * @param {Array} elements - Tous les éléments * @param {Object} csvData - Données CSV * @returns {Object} Analyse contextuelle */ function analyzeAvailableContext(elements, csvData) { const availableKeywords = []; const availableContent = []; // ✅ 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') && !element.resolvedContent.includes('non résolu') && element.resolvedContent.trim() !== '') { if (element.type.includes('titre')) { availableKeywords.push(element.resolvedContent); } else { availableContent.push(element.resolvedContent.substring(0, 100)); } } }); return { mainKeyword: csvData.mc0, mainTitle: csvData.t0, availableKeywords: availableKeywords, // ✅ Maintenant inclut csvData + elements availableContent: availableContent, theme: csvData.mc0 // Thème principal }; } /** * Obtenir le contexte spécifique d'un élément * @param {Object} element - Élément à analyser * @param {Array} allElements - Tous les éléments * @param {Object} csvData - Données CSV * @returns {Object} Contexte de l'élément */ function getElementContext(element, allElements, csvData) { const context = { elementType: element.type, 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_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 => { // 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 * @param {Object} contextAnalysis - Analyse contextuelle * @param {Object} csvData - Données CSV avec personnalité * @returns {Object} Mots-clés générés */ 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 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) => { 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 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 EXEMPLES DE DIVERSITÉ ATTENDUE: - ❌ MAL: "Plaque dibond", "Plaque aluminium", "Plaque personnalisée" - ✅ BIEN: "Dibond haute résistance", "Finitions gravure laser", "Sur-mesure extérieur" ⚠️ 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. 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('🤖 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: 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}`); } } /** * Parser la réponse IA pour extraire les mots-clés générés * @param {string} response - Réponse de l'IA * @param {Array} missingElements - Éléments manquants * @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(); let generatedKeyword = match[2].trim(); // ✅ 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`); } // ✅ 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'); } } 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 * @param {Object} generatedKeywords - Nouveaux mots-clés * @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]; // 🔍 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(`✅ ${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, generateMissingSheetVariables // ✅ Export de la nouvelle fonction };