This commit improves keyword generation by providing hierarchical context for each element and fixing the LLM response format parsing. Changes: 1. lib/MissingKeywords.js: - Add buildHierarchicalContext() to generate compact contextual info for each element - Display hierarchy in prompt (e.g., "H2 existants: 'Titre1', 'Titre2'") - For Txt elements: show associated MC keyword + parent title - For FAQ elements: count existing FAQs - Fix LLM response format by providing 3 concrete examples from actual list - Add explicit warning to use exact tag names [Titre_H2_3], [Txt_H2_6] - Improve getElementContext() to better retrieve hierarchical elements 2. lib/selective-enhancement/SelectiveUtils.js: - Fix createTypedPrompt() to use specific keyword from resolvedContent - Remove fallback to csvData.mc0 (log error if no specific keyword) 3. lib/pipeline/PipelineExecutor.js: - Integrate generateMissingSheetVariables() as "Étape 0" before extraction Prompt format now: 1. [Titre_H2_3] (titre) — H2 existants: "Titre1", "Titre2" 2. [Txt_H2_6] (texte) — MC: "Plaque dibond" | Parent: "Guide dibond" 3. [Faq_q_1] (question) — 3 FAQ existantes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1031 lines
39 KiB
JavaScript
1031 lines
39 KiB
JavaScript
// ========================================
|
|
// 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('<?xml')
|
|
? xmlTemplate
|
|
: Buffer.from(xmlTemplate, 'base64').toString('utf8');
|
|
|
|
const requestedVariables = {
|
|
'MC+1': new Set(),
|
|
'T+1': new Set(),
|
|
'L+1': new Set(),
|
|
'MC-1': new Set(),
|
|
'T-1': new Set(),
|
|
'L-1': new Set()
|
|
};
|
|
|
|
// Chercher toutes les variables {{MC+1_X}}, {{T+1_X}}, {{L+1_X}}, etc.
|
|
const regex = /\{\{(MC|T|L)([+-])1_(\d+)\}\}/gi;
|
|
let match;
|
|
|
|
while ((match = regex.exec(xmlString)) !== null) {
|
|
const varType = match[1].toUpperCase(); // MC, T, L
|
|
const sign = match[2]; // + ou -
|
|
const index = parseInt(match[3]); // 1, 2, 3, etc.
|
|
const key = `${varType}${sign}1`;
|
|
|
|
requestedVariables[key].add(index);
|
|
}
|
|
|
|
// Convertir Sets en arrays triés
|
|
Object.keys(requestedVariables).forEach(key => {
|
|
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
|
|
}; |