Added plan.md with complete architecture for format-agnostic content generation: - Support for Markdown, HTML, Plain Text, JSON formats - New FormatExporter module with neutral data structure - Integration strategy with existing ContentAssembly and ArticleStorage - Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format - Implementation roadmap with 4 phases (6h total estimated) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1632 lines
63 KiB
JavaScript
1632 lines
63 KiB
JavaScript
// ========================================
|
|
// FICHIER: SelectiveEnhancement.js - Node.js Version
|
|
// Description: Enhancement par batch pour éviter timeouts
|
|
// ========================================
|
|
|
|
const { callLLM } = require('./LLMManager');
|
|
const { logSh } = require('./ErrorReporting');
|
|
const { tracer } = require('./trace.js');
|
|
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig');
|
|
|
|
// Utilitaire pour les délais
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
|
|
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection
|
|
*/
|
|
async function generateWithBatchEnhancement(hierarchy, csvData) {
|
|
const totalElements = Object.keys(hierarchy).length;
|
|
|
|
// NOUVEAU: Sélection de 4 personnalités complémentaires
|
|
const personalities = await tracer.run('SelectiveEnhancement.selectMultiplePersonalities()', async () => {
|
|
const allPersonalities = await getPersonalities();
|
|
const selectedPersonalities = await selectMultiplePersonalitiesWithAI(csvData.mc0, csvData.t0, allPersonalities);
|
|
await tracer.event(`4 personnalités sélectionnées: ${selectedPersonalities.map(p => p.nom).join(', ')}`);
|
|
return selectedPersonalities;
|
|
}, { mc0: csvData.mc0, t0: csvData.t0 });
|
|
|
|
await tracer.annotate({
|
|
totalElements,
|
|
personalities: personalities.map(p => `${p.nom}(${p.style})`).join(', '),
|
|
mc0: csvData.mc0
|
|
});
|
|
|
|
// ÉTAPE 1 : Génération base avec IA configurée + Personnalité 1
|
|
const baseContents = await tracer.run('SelectiveEnhancement.generateAllContentBase()', async () => {
|
|
const csvDataWithPersonality1 = { ...csvData, personality: personalities[0] };
|
|
const aiProvider1 = personalities[0].aiEtape1Base;
|
|
const result = await generateAllContentBase(hierarchy, csvDataWithPersonality1, aiProvider1);
|
|
await tracer.event(`${Object.keys(result).length} éléments générés avec ${personalities[0].nom} via ${aiProvider1.toUpperCase()}`);
|
|
return result;
|
|
}, { hierarchyElements: Object.keys(hierarchy).length, personality1: personalities[0].nom, llmProvider: personalities[0].aiEtape1Base, mc0: csvData.mc0 });
|
|
|
|
// ÉTAPE 2 : Enhancement technique avec IA configurée + Personnalité 2
|
|
const technicalEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTechnicalTerms()', async () => {
|
|
const csvDataWithPersonality2 = { ...csvData, personality: personalities[1] };
|
|
const aiProvider2 = personalities[1].aiEtape2Technique;
|
|
const result = await enhanceAllTechnicalTerms(baseContents, csvDataWithPersonality2, aiProvider2);
|
|
const enhancedCount = Object.keys(result).filter(k => result[k] !== baseContents[k]).length;
|
|
await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments techniques améliorés avec ${personalities[1].nom} via ${aiProvider2.toUpperCase()}`);
|
|
return result;
|
|
}, { baseElements: Object.keys(baseContents).length, personality2: personalities[1].nom, llmProvider: personalities[1].aiEtape2Technique, mc0: csvData.mc0 });
|
|
|
|
// ÉTAPE 3 : Enhancement transitions avec IA configurée + Personnalité 3
|
|
const transitionsEnhanced = await tracer.run('SelectiveEnhancement.enhanceAllTransitions()', async () => {
|
|
const csvDataWithPersonality3 = { ...csvData, personality: personalities[2] };
|
|
const aiProvider3 = personalities[2].aiEtape3Transitions;
|
|
const result = await enhanceAllTransitions(technicalEnhanced, csvDataWithPersonality3, aiProvider3);
|
|
const enhancedCount = Object.keys(result).filter(k => result[k] !== technicalEnhanced[k]).length;
|
|
await tracer.event(`${enhancedCount}/${Object.keys(result).length} transitions fluidifiées avec ${personalities[2].nom} via ${aiProvider3.toUpperCase()}`);
|
|
return result;
|
|
}, { technicalElements: Object.keys(technicalEnhanced).length, personality3: personalities[2].nom, llmProvider: personalities[2].aiEtape3Transitions });
|
|
|
|
// ÉTAPE 4 : Enhancement style avec IA configurée + Personnalité 4
|
|
const finalContents = await tracer.run('SelectiveEnhancement.enhanceAllPersonalityStyle()', async () => {
|
|
const csvDataWithPersonality4 = { ...csvData, personality: personalities[3] };
|
|
const aiProvider4 = personalities[3].aiEtape4Style;
|
|
const result = await enhanceAllPersonalityStyle(transitionsEnhanced, csvDataWithPersonality4, aiProvider4);
|
|
const enhancedCount = Object.keys(result).filter(k => result[k] !== transitionsEnhanced[k]).length;
|
|
const avgWords = Math.round(Object.values(result).reduce((acc, content) => acc + content.split(' ').length, 0) / Object.keys(result).length);
|
|
await tracer.event(`${enhancedCount}/${Object.keys(result).length} éléments stylisés avec ${personalities[3].nom} via ${aiProvider4.toUpperCase()}`, { avgWordsPerElement: avgWords });
|
|
return result;
|
|
}, { transitionElements: Object.keys(transitionsEnhanced).length, personality4: personalities[3].nom, llmProvider: personalities[3].aiEtape4Style });
|
|
|
|
// Log final du DNA Mixing réussi avec IA configurables
|
|
const aiChain = personalities.map((p, i) => `${p.aiEtape1Base || p.aiEtape2Technique || p.aiEtape3Transitions || p.aiEtape4Style}`.toUpperCase()).join(' → ');
|
|
logSh(`✅ DNA MIXING MULTI-PERSONNALITÉS TERMINÉ:`, 'INFO');
|
|
logSh(` 🎭 4 personnalités utilisées: ${personalities.map(p => p.nom).join(' → ')}`, 'INFO');
|
|
logSh(` 🤖 IA configurées: ${personalities[0].aiEtape1Base.toUpperCase()} → ${personalities[1].aiEtape2Technique.toUpperCase()} → ${personalities[2].aiEtape3Transitions.toUpperCase()} → ${personalities[3].aiEtape4Style.toUpperCase()}`, 'INFO');
|
|
logSh(` 📝 ${Object.keys(finalContents).length} éléments avec style hybride généré`, 'INFO');
|
|
|
|
return finalContents;
|
|
}
|
|
|
|
/**
|
|
* ÉTAPE 1 - Génération base TOUS éléments avec IA configurable
|
|
*/
|
|
async function generateAllContentBase(hierarchy, csvData, aiProvider) {
|
|
logSh('🔍 === DEBUG GÉNÉRATION BASE ===', 'DEBUG');
|
|
|
|
// Debug: logger la hiérarchie complète
|
|
logSh(`🔍 Hiérarchie reçue: ${Object.keys(hierarchy).length} sections`, 'DEBUG');
|
|
Object.keys(hierarchy).forEach((path, i) => {
|
|
const section = hierarchy[path];
|
|
logSh(`🔍 Section ${i+1} [${path}]:`, 'DEBUG');
|
|
logSh(`🔍 - title: ${section.title ? section.title.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG');
|
|
logSh(`🔍 - text: ${section.text ? section.text.originalElement?.originalTag : 'AUCUN'}`, 'DEBUG');
|
|
logSh(`🔍 - questions: ${section.questions?.length || 0}`, 'DEBUG');
|
|
});
|
|
|
|
const allElements = collectAllElements(hierarchy);
|
|
logSh(`🔍 Éléments collectés: ${allElements.length}`, 'DEBUG');
|
|
|
|
// Debug: logger tous les éléments collectés
|
|
allElements.forEach((element, i) => {
|
|
logSh(`🔍 Élément ${i+1}: tag="${element.tag}", type="${element.type}"`, 'DEBUG');
|
|
});
|
|
|
|
// NOUVELLE LOGIQUE : SÉPARER PAIRES FAQ ET AUTRES ÉLÉMENTS
|
|
const results = {};
|
|
|
|
logSh(`🔍 === GÉNÉRATION INTELLIGENTE DE ${allElements.length} ÉLÉMENTS ===`, 'DEBUG');
|
|
logSh(`🔍 Ordre respecté: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'DEBUG');
|
|
|
|
// 1. IDENTIFIER les paires FAQ
|
|
const { faqPairs, otherElements } = separateFAQPairsAndOthers(allElements);
|
|
|
|
logSh(`🔍 ${faqPairs.length} paires FAQ trouvées, ${otherElements.length} autres éléments`, 'INFO');
|
|
|
|
// 2. GÉNÉRER les autres éléments EN BATCH ORDONNÉ (titres d'abord, puis textes avec contexte)
|
|
const groupedElements = groupElementsByType(otherElements);
|
|
|
|
// ORDRE DE GÉNÉRATION : TITRES → TEXTES → INTRO → AUTRES
|
|
const orderedTypes = ['titre', 'texte', 'intro'];
|
|
|
|
for (const type of orderedTypes) {
|
|
const elements = groupedElements[type];
|
|
if (!elements || elements.length === 0) continue;
|
|
|
|
// DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS
|
|
const chunks = chunkArray(elements, 4);
|
|
logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO');
|
|
|
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
const chunk = chunks[chunkIndex];
|
|
logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
|
|
|
|
try {
|
|
// Passer les résultats déjà générés pour contexte (titres → textes)
|
|
const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results);
|
|
|
|
const batchResponse = await callLLM(aiProvider, batchPrompt, {
|
|
temperature: 0.7,
|
|
maxTokens: 2000 * chunk.length
|
|
}, csvData.personality);
|
|
|
|
const batchResults = parseBatchResponse(batchResponse, chunk);
|
|
Object.assign(results, batchResults);
|
|
|
|
logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR');
|
|
throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`);
|
|
}
|
|
|
|
// Délai entre chunks pour éviter rate limiting
|
|
if (chunkIndex < chunks.length - 1) {
|
|
await sleep(1500);
|
|
}
|
|
}
|
|
|
|
logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO');
|
|
}
|
|
|
|
// TRAITER les types restants (autres que titre/texte/intro)
|
|
for (const [type, elements] of Object.entries(groupedElements)) {
|
|
if (orderedTypes.includes(type) || elements.length === 0) continue;
|
|
|
|
// DÉCOUPER EN CHUNKS DE MAX 4 ÉLÉMENTS POUR ÉVITER TIMEOUTS
|
|
const chunks = chunkArray(elements, 4);
|
|
logSh(`🚀 BATCH ${type.toUpperCase()}: ${elements.length} éléments en ${chunks.length} chunks`, 'INFO');
|
|
|
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
const chunk = chunks[chunkIndex];
|
|
logSh(` Chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
|
|
|
|
try {
|
|
const batchPrompt = createBatchBasePrompt(chunk, type, csvData, results);
|
|
|
|
const batchResponse = await callLLM(aiProvider, batchPrompt, {
|
|
temperature: 0.7,
|
|
maxTokens: 2000 * chunk.length
|
|
}, csvData.personality);
|
|
|
|
const batchResults = parseBatchResponse(batchResponse, chunk);
|
|
Object.assign(results, batchResults);
|
|
|
|
logSh(`✅ Chunk ${chunkIndex + 1}: ${Object.keys(batchResults).length}/${chunk.length} éléments générés`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ FATAL: Chunk ${chunkIndex + 1} de ${type} échoué: ${error.message}`, 'ERROR');
|
|
throw new Error(`FATAL: Génération chunk ${chunkIndex + 1} de ${type} échouée - arrêt du workflow: ${error.message}`);
|
|
}
|
|
|
|
// Délai entre chunks
|
|
if (chunkIndex < chunks.length - 1) {
|
|
await sleep(1500);
|
|
}
|
|
}
|
|
|
|
logSh(`✅ BATCH ${type.toUpperCase()} COMPLET: ${elements.length} éléments générés en ${chunks.length} chunks`, 'INFO');
|
|
}
|
|
|
|
// 3. GÉNÉRER les paires FAQ ensemble (RESTAURÉ depuis .gs)
|
|
if (faqPairs.length > 0) {
|
|
logSh(`🔍 === GÉNÉRATION PAIRES FAQ (${faqPairs.length} paires) ===`, 'INFO');
|
|
const faqResults = await generateFAQPairsRestored(faqPairs, csvData, aiProvider);
|
|
Object.assign(results, faqResults);
|
|
}
|
|
|
|
logSh(`🔍 === RÉSULTATS FINAUX GÉNÉRATION BASE ===`, 'DEBUG');
|
|
logSh(`🔍 Total généré: ${Object.keys(results).length} éléments`, 'DEBUG');
|
|
Object.keys(results).forEach(tag => {
|
|
logSh(`🔍 [${tag}]: "${results[tag]}"`, 'DEBUG');
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
|
|
* NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
|
|
*/
|
|
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
|
|
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO');
|
|
logSh('Enhancement technique BATCH TOTAL...', 'DEBUG');
|
|
|
|
const allElements = Object.keys(baseContents);
|
|
if (allElements.length === 0) {
|
|
logSh('⚠️ Aucun élément à analyser techniquement', 'WARNING');
|
|
return baseContents;
|
|
}
|
|
|
|
const analysisStart = Date.now();
|
|
logSh(`📊 Analyse démarrée: ${allElements.length} éléments à examiner`, 'INFO');
|
|
|
|
try {
|
|
// ÉTAPE 1 : Extraction batch TOUS les termes techniques (1 seul appel)
|
|
logSh(`🔍 Analyse technique batch: ${allElements.length} éléments`, 'INFO');
|
|
const technicalAnalysis = await extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider);
|
|
const analysisEnd = Date.now();
|
|
|
|
// ÉTAPE 2 : Enhancement batch TOUS les éléments qui en ont besoin (1 seul appel)
|
|
const elementsNeedingEnhancement = technicalAnalysis.filter(item => item.needsEnhancement);
|
|
|
|
logSh(`📋 Analyse terminée (${analysisEnd - analysisStart}ms):`, 'INFO');
|
|
logSh(` • ${elementsNeedingEnhancement.length}/${allElements.length} éléments nécessitent enhancement`, 'INFO');
|
|
|
|
if (elementsNeedingEnhancement.length === 0) {
|
|
logSh('✅ Aucun élément ne nécessite enhancement technique - contenu déjà optimal', 'INFO');
|
|
return baseContents;
|
|
}
|
|
|
|
// Log détaillé des éléments à améliorer
|
|
elementsNeedingEnhancement.forEach((item, i) => {
|
|
logSh(` ${i+1}. [${item.tag}]: ${item.technicalTerms.join(', ')}`, 'DEBUG');
|
|
});
|
|
|
|
const enhancementStart = Date.now();
|
|
logSh(`🔧 Enhancement technique: ${elementsNeedingEnhancement.length}/${allElements.length} éléments`, 'INFO');
|
|
const enhancedContents = await enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider);
|
|
const enhancementEnd = Date.now();
|
|
|
|
// ÉTAPE 3 : Merger résultats
|
|
const results = { ...baseContents };
|
|
let actuallyEnhanced = 0;
|
|
Object.keys(enhancedContents).forEach(tag => {
|
|
if (enhancedContents[tag] !== baseContents[tag]) {
|
|
results[tag] = enhancedContents[tag];
|
|
actuallyEnhanced++;
|
|
}
|
|
});
|
|
|
|
logSh(`⚡ Enhancement terminé (${enhancementEnd - enhancementStart}ms):`, 'INFO');
|
|
logSh(` • ${actuallyEnhanced} éléments réellement améliorés`, 'INFO');
|
|
logSh(` • Termes intégrés: dibond, impression UV, fraisage, etc.`, 'DEBUG');
|
|
logSh(`✅ Enhancement technique terminé avec succès`, 'INFO');
|
|
return results;
|
|
|
|
} catch (error) {
|
|
const analysisTotal = Date.now() - analysisStart;
|
|
logSh(`❌ FATAL: Enhancement technique échoué après ${analysisTotal}ms`, 'ERROR');
|
|
logSh(`❌ Message: ${error.message}`, 'ERROR');
|
|
throw new Error(`FATAL: Enhancement technique impossible - arrêt du workflow: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyser un seul élément pour détecter les termes techniques
|
|
*/
|
|
async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
|
|
const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.
|
|
|
|
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
|
|
|
|
CONTENU À ANALYSER:
|
|
TAG: ${tag}
|
|
CONTENU: "${content}"
|
|
|
|
CONSIGNES:
|
|
- Cherche UNIQUEMENT des vrais termes techniques métier/industrie
|
|
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
|
|
- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques
|
|
|
|
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
|
|
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance
|
|
|
|
RÉPONSE REQUISE:
|
|
- Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
|
|
- Si aucun terme technique: "NON"
|
|
|
|
EXEMPLE:
|
|
OUI - termes: aluminium composite, impression numérique, gravure laser`;
|
|
|
|
try {
|
|
const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
|
|
|
|
if (response.toUpperCase().startsWith('OUI')) {
|
|
// Extraire les termes de la réponse
|
|
const termsMatch = response.match(/termes:\s*(.+)/i);
|
|
const terms = termsMatch ? termsMatch[1].trim() : '';
|
|
logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG');
|
|
return true;
|
|
} else {
|
|
logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG');
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
|
|
return false; // En cas d'erreur, on skip l'enhancement
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enhancer un seul élément techniquement
|
|
*/
|
|
async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
|
|
const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.
|
|
|
|
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
|
|
|
|
CONTENU À AMÉLIORER:
|
|
TAG: ${tag}
|
|
CONTENU: "${content}"
|
|
|
|
OBJECTIFS:
|
|
- Remplace les termes génériques par des termes techniques précis
|
|
- Ajoute des spécifications techniques réalistes
|
|
- Maintient le même style et longueur
|
|
- Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes
|
|
|
|
EXEMPLE DE TRANSFORMATION:
|
|
"matériaux haute performance" → "dibond 3mm ou aluminium composite"
|
|
"impression moderne" → "impression UV haute définition"
|
|
"fixation solide" → "fixation par chevilles inox Ø6mm"
|
|
|
|
CONTRAINTES:
|
|
- GARDE la même structure
|
|
- MÊME longueur approximative
|
|
- Style cohérent avec l'original
|
|
- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;
|
|
|
|
try {
|
|
const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
|
|
return enhancedContent.trim();
|
|
} catch (error) {
|
|
logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
|
|
return content; // En cas d'erreur, on retourne le contenu original
|
|
}
|
|
}
|
|
|
|
// ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL
|
|
|
|
/**
|
|
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
|
*/
|
|
// FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel
|
|
|
|
/**
|
|
* ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable
|
|
*/
|
|
async function enhanceAllTransitions(baseContents, csvData, aiProvider) {
|
|
logSh('🔗 === DÉBUT ENHANCEMENT TRANSITIONS ===', 'INFO');
|
|
logSh('Enhancement transitions batch...', 'DEBUG');
|
|
|
|
const transitionStart = Date.now();
|
|
const allElements = Object.keys(baseContents);
|
|
logSh(`📊 Analyse transitions: ${allElements.length} éléments à examiner`, 'INFO');
|
|
|
|
// Sélectionner éléments longs qui bénéficient d'amélioration transitions
|
|
const transitionElements = [];
|
|
let analyzedCount = 0;
|
|
Object.keys(baseContents).forEach(tag => {
|
|
const content = baseContents[tag];
|
|
analyzedCount++;
|
|
if (content.length > 150) {
|
|
const needsTransitions = analyzeTransitionNeed(content);
|
|
logSh(` [${tag}]: ${content.length}c, transitions=${needsTransitions ? '✅' : '❌'}`, 'DEBUG');
|
|
if (needsTransitions) {
|
|
transitionElements.push({
|
|
tag: tag,
|
|
content: content
|
|
});
|
|
}
|
|
} else {
|
|
logSh(` [${tag}]: ${content.length}c - trop court, ignoré`, 'DEBUG');
|
|
}
|
|
});
|
|
|
|
logSh(`📋 Analyse transitions terminée:`, 'INFO');
|
|
logSh(` • ${analyzedCount} éléments analysés`, 'INFO');
|
|
logSh(` • ${transitionElements.length} nécessitent amélioration`, 'INFO');
|
|
|
|
if (transitionElements.length === 0) {
|
|
logSh('✅ Pas d\'éléments nécessitant enhancement transitions - fluidité déjà optimale', 'INFO');
|
|
return baseContents;
|
|
}
|
|
|
|
logSh(`${transitionElements.length} éléments à améliorer (transitions)`, 'INFO');
|
|
|
|
const chunks = chunkArray(transitionElements, 6); // Plus petit pour Gemini
|
|
const results = { ...baseContents };
|
|
|
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
const chunk = chunks[chunkIndex];
|
|
|
|
try {
|
|
logSh(`Chunk transitions ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG');
|
|
|
|
const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
|
|
|
|
CONTEXTE: Article SEO professionnel pour site web commercial
|
|
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} adapté web)
|
|
CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref}
|
|
|
|
CONTENUS:
|
|
|
|
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
|
"${item.content}"`).join('\n\n')}
|
|
|
|
OBJECTIFS:
|
|
- Connecteurs plus naturels et variés issus de: ${csvData.personality?.connecteursPref}
|
|
- Transitions fluides entre idées
|
|
- ÉVITE répétitions excessives ("franchement", "du coup", "vraiment", "par ailleurs")
|
|
- Style cohérent ${csvData.personality?.style}
|
|
|
|
CONTRAINTES STRICTES:
|
|
- NE CHANGE PAS le fond du message
|
|
- GARDE la même structure et longueur approximative
|
|
- Améliore SEULEMENT la fluidité des transitions
|
|
- RESPECTE le style ${csvData.personality?.nom}
|
|
- RÉPONDS DIRECTEMENT PAR LE CONTENU AMÉLIORÉ, sans préfixe ni tag XML
|
|
|
|
FORMAT DE RÉPONSE:
|
|
[1] Contenu avec transitions améliorées selon ${csvData.personality?.nom}
|
|
[2] Contenu avec transitions améliorées selon ${csvData.personality?.nom}
|
|
etc...`;
|
|
|
|
const improved = await callLLM(aiProvider, batchTransitionsPrompt, {
|
|
temperature: 0.6,
|
|
maxTokens: 2500
|
|
}, csvData.personality);
|
|
|
|
const parsedImprovements = parseTransitionsBatchResponse(improved, chunk);
|
|
|
|
Object.keys(parsedImprovements).forEach(tag => {
|
|
results[tag] = parsedImprovements[tag];
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur chunk transitions ${chunkIndex + 1}: ${error.message}`, 'ERROR');
|
|
}
|
|
|
|
if (chunkIndex < chunks.length - 1) {
|
|
await sleep(1500);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* ÉTAPE 4 - Enhancement style personnalité BATCH avec IA configurable
|
|
*/
|
|
async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {
|
|
const personality = csvData.personality;
|
|
if (!personality) {
|
|
logSh('Pas de personnalité, skip enhancement style', 'DEBUG');
|
|
return baseContents;
|
|
}
|
|
|
|
logSh(`Enhancement style ${personality.nom} batch...`, 'DEBUG');
|
|
|
|
// Tous les éléments bénéficient de l'adaptation personnalité
|
|
const styleElements = Object.keys(baseContents).map(tag => ({
|
|
tag: tag,
|
|
content: baseContents[tag]
|
|
}));
|
|
|
|
const chunks = chunkArray(styleElements, 8);
|
|
const results = { ...baseContents };
|
|
|
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
const chunk = chunks[chunkIndex];
|
|
|
|
try {
|
|
logSh(`Chunk style ${chunkIndex + 1}/${chunks.length} (${chunk.length} éléments)`, 'DEBUG');
|
|
|
|
const batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
|
|
|
|
CONTEXTE: Finalisation article SEO pour site e-commerce professionnel
|
|
PERSONNALITÉ: ${personality.nom}
|
|
DESCRIPTION: ${personality.description}
|
|
STYLE CIBLE: ${personality.style} adapté au web professionnel
|
|
VOCABULAIRE: ${personality.vocabulairePref}
|
|
CONNECTEURS: ${personality.connecteursPref}
|
|
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
|
|
LONGUEUR PHRASES: ${personality.longueurPhrases}
|
|
|
|
CONTENUS À STYLISER:
|
|
|
|
${chunk.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
|
"${item.content}"`).join('\n\n')}
|
|
|
|
CONSIGNES STRICTES:
|
|
- GARDE le même contenu informatif et technique
|
|
- Adapte SEULEMENT le ton, les expressions et le vocabulaire selon ${personality.nom}
|
|
- RESPECTE la longueur approximative (même nombre de mots ±20%)
|
|
- ÉVITE les répétitions excessives ("franchement", "du coup", "vraiment")
|
|
- VARIE les expressions et connecteurs selon: ${personality.connecteursPref}
|
|
- Style ${personality.nom} reconnaissable mais NATUREL
|
|
- RÉPONDS DIRECTEMENT PAR LE CONTENU STYLISÉ, sans préfixe ni tag XML
|
|
- PAS de messages d'excuse ou d'incapacité
|
|
|
|
FORMAT DE RÉPONSE:
|
|
[1] Contenu stylisé selon ${personality.nom} (${personality.style})
|
|
[2] Contenu stylisé selon ${personality.nom} (${personality.style})
|
|
etc...`;
|
|
|
|
const styled = await callLLM(aiProvider, batchStylePrompt, {
|
|
temperature: 0.8,
|
|
maxTokens: 3000
|
|
}, personality);
|
|
|
|
const parsedStyles = parseStyleBatchResponse(styled, chunk);
|
|
|
|
Object.keys(parsedStyles).forEach(tag => {
|
|
results[tag] = parsedStyles[tag];
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur chunk style ${chunkIndex + 1}: ${error.message}`, 'ERROR');
|
|
}
|
|
|
|
if (chunkIndex < chunks.length - 1) {
|
|
await sleep(1500);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// ============= HELPER FUNCTIONS =============
|
|
|
|
/**
|
|
* Sleep function replacement for Utilities.sleep
|
|
*/
|
|
|
|
// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12
|
|
|
|
/**
|
|
* RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
|
|
*/
|
|
async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) {
|
|
logSh(`🔍 === GÉNÉRATION PAIRES FAQ (logique .gs restaurée) ===`, 'INFO');
|
|
|
|
if (faqPairs.length === 0) return {};
|
|
|
|
const batchPrompt = createBatchFAQPairsPrompt(faqPairs, csvData);
|
|
logSh(`🔍 Prompt FAQ paires (${batchPrompt.length} chars): "${batchPrompt.substring(0, 300)}..."`, 'DEBUG');
|
|
|
|
try {
|
|
const batchResponse = await callLLM(aiProvider, batchPrompt, {
|
|
temperature: 0.8,
|
|
maxTokens: 3000 // Plus large pour les paires
|
|
}, csvData.personality);
|
|
|
|
logSh(`🔍 Réponse FAQ paires reçue: ${batchResponse.length} caractères`, 'DEBUG');
|
|
logSh(`🔍 Début réponse: "${batchResponse.substring(0, 200)}..."`, 'DEBUG');
|
|
|
|
return parseFAQPairsResponse(batchResponse, faqPairs);
|
|
|
|
} catch (error) {
|
|
logSh(`❌ FATAL: Erreur génération paires FAQ: ${error.message}`, 'ERROR');
|
|
throw new Error(`FATAL: Génération paires FAQ échouée - arrêt du workflow: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* RESTAURÉ DEPUIS .GS : Prompt pour paires FAQ cohérentes
|
|
*/
|
|
function createBatchFAQPairsPrompt(faqPairs, csvData) {
|
|
const personality = csvData.personality;
|
|
|
|
let prompt = `=== 1. CONTEXTE ===
|
|
Entreprise: Autocollant.fr - signalétique personnalisée
|
|
Sujet: ${csvData.mc0}
|
|
Section: FAQ pour article SEO commercial
|
|
|
|
=== 2. PERSONNALITÉ ===
|
|
Rédacteur: ${personality.nom}
|
|
Style: ${personality.style}
|
|
Ton: ${personality.description || 'professionnel'}
|
|
|
|
=== 3. RÈGLES GÉNÉRALES ===
|
|
- Questions naturelles de clients
|
|
- Réponses expertes et rassurantes
|
|
- Langage professionnel mais accessible
|
|
- Textes rédigés humainement et de façon authentique
|
|
- Couvrir: prix, livraison, personnalisation, installation, durabilité
|
|
- IMPÉRATIF: Respecter strictement les contraintes XML
|
|
|
|
=== 4. PAIRES FAQ À GÉNÉRER ===
|
|
|
|
`;
|
|
|
|
faqPairs.forEach((pair, index) => {
|
|
const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
|
const answerTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
|
|
|
prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle
|
|
`;
|
|
});
|
|
|
|
prompt += `
|
|
|
|
FORMAT DE RÉPONSE:
|
|
PAIRE 1:
|
|
[${faqPairs[0].question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
|
|
Question client directe et naturelle sur ${csvData.mc0} ?
|
|
|
|
[${faqPairs[0].answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
|
|
Réponse utile et rassurante selon le style ${personality.style} de ${personality.nom}.
|
|
`;
|
|
|
|
if (faqPairs.length > 1) {
|
|
prompt += `PAIRE 2:
|
|
etc...
|
|
`;
|
|
}
|
|
|
|
return prompt;
|
|
}
|
|
|
|
/**
|
|
* RESTAURÉ DEPUIS .GS : Parser réponse paires FAQ
|
|
*/
|
|
function parseFAQPairsResponse(response, faqPairs) {
|
|
const results = {};
|
|
|
|
logSh(`🔍 Parsing FAQ paires: "${response.substring(0, 300)}..."`, 'DEBUG');
|
|
|
|
// Parser avec regex [TAG] contenu
|
|
const regex = /\[([^\]]+)\]\s*([^[]*?)(?=\[|$)/gs;
|
|
let match;
|
|
const parsedItems = {};
|
|
|
|
while ((match = regex.exec(response)) !== null) {
|
|
const tag = match[1].trim();
|
|
let content = match[2].trim().replace(/\n\s*\n/g, '\n').replace(/^\n+|\n+$/g, '');
|
|
|
|
// NOUVEAU: Appliquer le nettoyage XML pour FAQ aussi
|
|
content = cleanXMLTagsFromContent(content);
|
|
|
|
if (content && content.length > 0) {
|
|
parsedItems[tag] = content;
|
|
logSh(`🔍 Parsé [${tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
|
|
}
|
|
}
|
|
|
|
// Mapper aux vrais tags FAQ avec |
|
|
let pairesCompletes = 0;
|
|
faqPairs.forEach(pair => {
|
|
const questionCleanTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
|
const answerCleanTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
|
|
|
const questionContent = parsedItems[questionCleanTag];
|
|
const answerContent = parsedItems[answerCleanTag];
|
|
|
|
if (questionContent && answerContent) {
|
|
results[pair.question.tag] = questionContent;
|
|
results[pair.answer.tag] = answerContent;
|
|
pairesCompletes++;
|
|
logSh(`✅ Paire FAQ ${pair.number} complète: Q="${questionContent}" R="${answerContent.substring(0, 50)}..."`, 'INFO');
|
|
} else {
|
|
logSh(`⚠️ Paire FAQ ${pair.number} incomplète: Q=${!!questionContent} R=${!!answerContent}`, 'WARNING');
|
|
|
|
if (questionContent) results[pair.question.tag] = questionContent;
|
|
if (answerContent) results[pair.answer.tag] = answerContent;
|
|
}
|
|
});
|
|
|
|
logSh(`📊 FAQ parsing: ${pairesCompletes}/${faqPairs.length} paires complètes`, 'INFO');
|
|
|
|
// FATAL si aucune paire complète (comme dans le .gs)
|
|
if (pairesCompletes === 0 && faqPairs.length > 0) {
|
|
logSh(`❌ FATAL: Aucune paire FAQ générée correctement`, 'ERROR');
|
|
throw new Error(`FATAL: Génération FAQ incomplète (0/${faqPairs.length} paires complètes) - arrêt du workflow`);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* RESTAURÉ DEPUIS .GS : Nettoyer instructions FAQ
|
|
*/
|
|
function cleanFAQInstructions(instructions, csvData) {
|
|
if (!instructions) return '';
|
|
|
|
let cleanInstructions = instructions;
|
|
|
|
// Remplacer variables
|
|
cleanInstructions = cleanInstructions.replace(/\{\{T0\}\}/g, csvData.t0 || '');
|
|
cleanInstructions = cleanInstructions.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
|
|
cleanInstructions = cleanInstructions.replace(/\{\{T-1\}\}/g, csvData.tMinus1 || '');
|
|
cleanInstructions = cleanInstructions.replace(/\{\{L-1\}\}/g, csvData.lMinus1 || '');
|
|
|
|
// Variables multiples MC+1_X, T+1_X, L+1_X
|
|
if (csvData.mcPlus1) {
|
|
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
|
|
for (let i = 1; i <= 6; i++) {
|
|
const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
|
|
cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
|
|
}
|
|
}
|
|
|
|
if (csvData.tPlus1) {
|
|
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
|
|
for (let i = 1; i <= 6; i++) {
|
|
const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
|
|
cleanInstructions = cleanInstructions.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
|
|
}
|
|
}
|
|
|
|
// Nettoyer HTML
|
|
cleanInstructions = cleanInstructions.replace(/<\/?[^>]+>/g, '');
|
|
cleanInstructions = cleanInstructions.replace(/\s+/g, ' ').trim();
|
|
|
|
return cleanInstructions;
|
|
}
|
|
|
|
/**
|
|
* Collecter tous les éléments dans l'ordre XML original
|
|
* CORRECTION: Suit l'ordre séquentiel XML au lieu de grouper par section
|
|
*/
|
|
function collectAllElements(hierarchy) {
|
|
const allElements = [];
|
|
const tagToElementMap = {};
|
|
|
|
// 1. Créer un mapping de tous les éléments disponibles
|
|
Object.keys(hierarchy).forEach(path => {
|
|
const section = hierarchy[path];
|
|
|
|
if (section.title) {
|
|
tagToElementMap[section.title.originalElement.originalTag] = {
|
|
tag: section.title.originalElement.originalTag,
|
|
element: section.title.originalElement,
|
|
type: 'titre'
|
|
};
|
|
}
|
|
|
|
if (section.text) {
|
|
tagToElementMap[section.text.originalElement.originalTag] = {
|
|
tag: section.text.originalElement.originalTag,
|
|
element: section.text.originalElement,
|
|
type: 'texte'
|
|
};
|
|
}
|
|
|
|
section.questions.forEach(q => {
|
|
tagToElementMap[q.originalElement.originalTag] = {
|
|
tag: q.originalElement.originalTag,
|
|
element: q.originalElement,
|
|
type: q.originalElement.type
|
|
};
|
|
});
|
|
});
|
|
|
|
// 2. Récupérer l'ordre XML original depuis le template global
|
|
logSh(`🔍 Global XML Template disponible: ${!!global.currentXmlTemplate}`, 'DEBUG');
|
|
if (global.currentXmlTemplate && global.currentXmlTemplate.length > 0) {
|
|
logSh(`🔍 Template XML: ${global.currentXmlTemplate.substring(0, 200)}...`, 'DEBUG');
|
|
const regex = /\|([^|]+)\|/g;
|
|
let match;
|
|
|
|
// Parcourir le XML dans l'ordre d'apparition
|
|
while ((match = regex.exec(global.currentXmlTemplate)) !== null) {
|
|
const fullMatch = match[1];
|
|
|
|
// Extraire le nom du tag (sans variables)
|
|
const nameMatch = fullMatch.match(/^([^{]+)/);
|
|
const tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0];
|
|
const pureTag = `|${tagName}|`;
|
|
|
|
// Si cet élément existe dans notre mapping, l'ajouter dans l'ordre
|
|
if (tagToElementMap[pureTag]) {
|
|
allElements.push(tagToElementMap[pureTag]);
|
|
logSh(`🔍 Ajouté dans l'ordre: ${pureTag}`, 'DEBUG');
|
|
delete tagToElementMap[pureTag]; // Éviter les doublons
|
|
} else {
|
|
logSh(`🔍 Tag XML non trouvé dans mapping: ${pureTag}`, 'DEBUG');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Ajouter les éléments restants (sécurité)
|
|
const remainingElements = Object.values(tagToElementMap);
|
|
if (remainingElements.length > 0) {
|
|
logSh(`🔍 Éléments restants ajoutés: ${remainingElements.map(el => el.tag).join(', ')}`, 'DEBUG');
|
|
remainingElements.forEach(element => {
|
|
allElements.push(element);
|
|
});
|
|
}
|
|
|
|
logSh(`🔍 ORDRE FINAL: ${allElements.map(el => el.tag.replace(/\|/g, '')).join(' → ')}`, 'INFO');
|
|
|
|
return allElements;
|
|
}
|
|
|
|
/**
|
|
* RESTAURÉ DEPUIS .GS : Séparer les paires FAQ des autres éléments
|
|
*/
|
|
function separateFAQPairsAndOthers(allElements) {
|
|
const faqPairs = [];
|
|
const otherElements = [];
|
|
const faqQuestions = {};
|
|
const faqAnswers = {};
|
|
|
|
// 1. Collecter toutes les questions et réponses FAQ
|
|
allElements.forEach(element => {
|
|
if (element.type === 'faq_question') {
|
|
// Extraire le numéro : |Faq_q_1| → 1
|
|
const numberMatch = element.tag.match(/(\d+)/);
|
|
const faqNumber = numberMatch ? numberMatch[1] : '1';
|
|
faqQuestions[faqNumber] = element;
|
|
logSh(`🔍 Question FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG');
|
|
} else if (element.type === 'faq_reponse') {
|
|
// Extraire le numéro : |Faq_a_1| → 1
|
|
const numberMatch = element.tag.match(/(\d+)/);
|
|
const faqNumber = numberMatch ? numberMatch[1] : '1';
|
|
faqAnswers[faqNumber] = element;
|
|
logSh(`🔍 Réponse FAQ ${faqNumber} trouvée: ${element.tag}`, 'DEBUG');
|
|
} else {
|
|
// Élément normal (titre, texte, intro, etc.)
|
|
otherElements.push(element);
|
|
}
|
|
});
|
|
|
|
// 2. Créer les paires FAQ cohérentes
|
|
Object.keys(faqQuestions).forEach(number => {
|
|
const question = faqQuestions[number];
|
|
const answer = faqAnswers[number];
|
|
|
|
if (question && answer) {
|
|
faqPairs.push({
|
|
number: number,
|
|
question: question,
|
|
answer: answer
|
|
});
|
|
logSh(`✅ Paire FAQ ${number} créée: ${question.tag} + ${answer.tag}`, 'INFO');
|
|
} else if (question) {
|
|
logSh(`⚠️ Question FAQ ${number} sans réponse correspondante`, 'WARNING');
|
|
otherElements.push(question); // Traiter comme élément individuel
|
|
} else if (answer) {
|
|
logSh(`⚠️ Réponse FAQ ${number} sans question correspondante`, 'WARNING');
|
|
otherElements.push(answer); // Traiter comme élément individuel
|
|
}
|
|
});
|
|
|
|
logSh(`🔍 Séparation terminée: ${faqPairs.length} paires FAQ, ${otherElements.length} autres éléments`, 'INFO');
|
|
|
|
return { faqPairs, otherElements };
|
|
}
|
|
|
|
/**
|
|
* Grouper éléments par type
|
|
*/
|
|
function groupElementsByType(elements) {
|
|
const groups = {};
|
|
|
|
elements.forEach(element => {
|
|
const type = element.type;
|
|
if (!groups[type]) {
|
|
groups[type] = [];
|
|
}
|
|
groups[type].push(element);
|
|
});
|
|
|
|
return groups;
|
|
}
|
|
|
|
/**
|
|
* Diviser array en chunks
|
|
*/
|
|
function chunkArray(array, size) {
|
|
const chunks = [];
|
|
for (let i = 0; i < array.length; i += size) {
|
|
chunks.push(array.slice(i, i + size));
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
/**
|
|
* Trouver le titre associé à un élément texte
|
|
*/
|
|
function findAssociatedTitle(textElement, existingResults) {
|
|
const textName = textElement.element.name || textElement.tag;
|
|
|
|
// STRATÉGIE 1: Correspondance directe (Txt_H2_1 → Titre_H2_1)
|
|
const directMatch = textName.replace(/Txt_/, 'Titre_').replace(/Text_/, 'Titre_');
|
|
const directTitle = existingResults[`|${directMatch}|`] || existingResults[directMatch];
|
|
if (directTitle) return directTitle;
|
|
|
|
// STRATÉGIE 2: Même niveau hiérarchique (H2, H3)
|
|
const levelMatch = textName.match(/(H\d)_(\d+)/);
|
|
if (levelMatch) {
|
|
const [, level, number] = levelMatch;
|
|
const titleTag = `Titre_${level}_${number}`;
|
|
const levelTitle = existingResults[`|${titleTag}|`] || existingResults[titleTag];
|
|
if (levelTitle) return levelTitle;
|
|
}
|
|
|
|
// STRATÉGIE 3: Proximité dans l'ordre (texte suivant un titre)
|
|
const allTitles = Object.entries(existingResults)
|
|
.filter(([tag]) => tag.includes('Titre'))
|
|
.sort(([a], [b]) => a.localeCompare(b));
|
|
|
|
if (allTitles.length > 0) {
|
|
// Retourner le premier titre disponible comme contexte général
|
|
return allTitles[0][1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Créer prompt batch de base
|
|
*/
|
|
function createBatchBasePrompt(elements, type, csvData, existingResults = {}) {
|
|
const personality = csvData.personality;
|
|
|
|
let prompt = `=== 1. CONTEXTE ===
|
|
Entreprise: Autocollant.fr - signalétique personnalisée
|
|
Sujet: ${csvData.mc0}
|
|
Type d'article: SEO professionnel pour site commercial
|
|
|
|
=== 2. PERSONNALITÉ ===
|
|
Rédacteur: ${personality.nom}
|
|
Style: ${personality.style}
|
|
Ton: ${personality.description || 'professionnel'}
|
|
|
|
=== 3. RÈGLES GÉNÉRALES ===
|
|
- Contenu SEO optimisé
|
|
- Langage naturel et fluide
|
|
- Éviter répétitions
|
|
- Pas de références techniques dans le contenu
|
|
- Textes rédigés humainement et de façon authentique
|
|
- IMPÉRATIF: Respecter strictement les contraintes XML (nombre de mots, etc.)
|
|
|
|
=== 4. ÉLÉMENTS À GÉNÉRER ===
|
|
`;
|
|
|
|
// AJOUTER CONTEXTE DES TITRES POUR LES TEXTES
|
|
if (type === 'texte' && Object.keys(existingResults).length > 0) {
|
|
const generatedTitles = Object.entries(existingResults)
|
|
.filter(([tag]) => tag.includes('Titre'))
|
|
.map(([tag, title]) => `• ${tag.replace(/\|/g, '')}: "${title}"`)
|
|
.slice(0, 5); // Limiter à 5 titres pour éviter surcharge
|
|
|
|
if (generatedTitles.length > 0) {
|
|
prompt += `
|
|
Titres existants pour contexte:
|
|
${generatedTitles.join('\n')}
|
|
|
|
`;
|
|
}
|
|
}
|
|
|
|
elements.forEach((elementInfo, index) => {
|
|
const cleanTag = elementInfo.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
|
|
|
prompt += `${index + 1}. [${cleanTag}] `;
|
|
|
|
// INSTRUCTIONS PROPRES PAR ÉLÉMENT
|
|
if (type === 'titre') {
|
|
if (elementInfo.element.type === 'titre_h1') {
|
|
prompt += `Titre principal accrocheur\n`;
|
|
} else if (elementInfo.element.type === 'titre_h2') {
|
|
prompt += `Titre de section engageant\n`;
|
|
} else if (elementInfo.element.type === 'titre_h3') {
|
|
prompt += `Sous-titre spécialisé\n`;
|
|
} else {
|
|
prompt += `Titre pertinent\n`;
|
|
}
|
|
} else if (type === 'texte') {
|
|
prompt += `Paragraphe informatif\n`;
|
|
|
|
// ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT
|
|
const associatedTitle = findAssociatedTitle(elementInfo, existingResults);
|
|
if (associatedTitle) {
|
|
prompt += ` Contexte: "${associatedTitle}"\n`;
|
|
}
|
|
|
|
if (elementInfo.element.resolvedContent) {
|
|
prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`;
|
|
}
|
|
} else if (type === 'intro') {
|
|
prompt += `Introduction engageante\n`;
|
|
} else {
|
|
prompt += `Contenu pertinent\n`;
|
|
}
|
|
});
|
|
|
|
prompt += `\nSTYLE ${personality.nom.toUpperCase()} - ${personality.style}:
|
|
- Vocabulaire: ${personality.vocabulairePref}
|
|
- Connecteurs: ${personality.connecteursPref}
|
|
- Phrases: ${personality.longueurPhrases}
|
|
- Niveau technique: ${personality.niveauTechnique}
|
|
|
|
CONSIGNES STRICTES POUR ARTICLE SEO:
|
|
- CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels
|
|
- STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel
|
|
- INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top")
|
|
- VOCABULAIRE: Mélange expertise technique + accessibilité client
|
|
- SEO: Utilise naturellement "${csvData.mc0}" et termes associés
|
|
- POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7"
|
|
- EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024"
|
|
- CONTENU: Informatif, rassurant, incite à l'achat SANS être trop commercial
|
|
- RÉPONDS DIRECTEMENT par le contenu web demandé, SANS préfixe
|
|
|
|
FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}:
|
|
[${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
|
|
${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H1_1")' : 'Contenu rédigé selon le style ' + personality.nom}
|
|
|
|
[${elements[1] ? elements[1].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '') : 'element2'}]
|
|
${type === 'titre' ? 'Titre réel et attractif (PAS "Titre_H2_1")' : 'Contenu rédigé selon le style ' + personality.nom}
|
|
|
|
etc...`;
|
|
|
|
return prompt;
|
|
}
|
|
|
|
/**
|
|
* Parser réponse batch générique avec nettoyage des tags XML
|
|
*/
|
|
function parseBatchResponse(response, elements) {
|
|
const results = {};
|
|
|
|
// Parser avec regex [TAG] contenu
|
|
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
|
|
let match;
|
|
const parsedItems = {};
|
|
|
|
while ((match = regex.exec(response)) !== null) {
|
|
const tag = match[1].trim();
|
|
let content = match[2].trim();
|
|
|
|
// NOUVEAU: Nettoyer les tags XML qui peuvent apparaître dans le contenu
|
|
content = cleanXMLTagsFromContent(content);
|
|
|
|
parsedItems[tag] = content;
|
|
}
|
|
|
|
// Mapper aux vrais tags avec |
|
|
elements.forEach(element => {
|
|
const cleanTag = element.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
|
|
|
if (parsedItems[cleanTag] && parsedItems[cleanTag].length > 10) {
|
|
results[element.tag] = parsedItems[cleanTag];
|
|
logSh(`✅ Parsé [${cleanTag}]: "${parsedItems[cleanTag].substring(0, 100)}..."`, 'DEBUG');
|
|
} else {
|
|
// Fallback si parsing échoue ou contenu trop court
|
|
results[element.tag] = `Contenu professionnel pour ${element.element.name}`;
|
|
logSh(`⚠️ Fallback [${cleanTag}]: parsing échoué ou contenu invalide`, 'WARNING');
|
|
}
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* NOUVELLE FONCTION: Nettoyer les tags XML du contenu généré
|
|
*/
|
|
function cleanXMLTagsFromContent(content) {
|
|
if (!content) return content;
|
|
|
|
// Supprimer les tags XML avec **
|
|
content = content.replace(/\*\*[^*]+\*\*/g, '');
|
|
|
|
// Supprimer les préfixes de titres indésirables
|
|
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?(voici\s+le\s+topo\s+pour\s+)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
|
|
content = content.replace(/^(Bon,?\s*)?(alors,?\s*)?pour\s+Titre_[HU]\d+_\d+[.,\s]*/gi, '');
|
|
content = content.replace(/^(Bon,?\s*)?(donc,?\s*)?Titre_[HU]\d+_\d+[.,\s]*/gi, '');
|
|
|
|
// Supprimer les messages d'excuse
|
|
content = content.replace(/Oh là là,?\s*je\s*(suis\s*)?(\w+\s*)?désolée?\s*,?\s*mais\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, '');
|
|
content = content.replace(/Bon,?\s*passons\s*au\s*suivant.*?(?=\.|$)/gi, '');
|
|
content = content.replace(/je\s*ne\s*sais\s*pas\s*quoi\s*vous\s*dire.*?(?=\.|$)/gi, '');
|
|
content = content.replace(/encore\s*un\s*point\s*où\s*je\s*n'ai\s*pas\s*l'information.*?(?=\.|$)/gi, '');
|
|
|
|
// Réduire les répétitions excessives d'expressions familières
|
|
content = content.replace(/(du coup[,\s]+){3,}/gi, 'du coup ');
|
|
content = content.replace(/(bon[,\s]+){3,}/gi, 'bon ');
|
|
content = content.replace(/(franchement[,\s]+){3,}/gi, 'franchement ');
|
|
content = content.replace(/(alors[,\s]+){3,}/gi, 'alors ');
|
|
content = content.replace(/(nickel[,\s]+){2,}/gi, 'nickel ');
|
|
content = content.replace(/(tip-top[,\s]+){2,}/gi, 'tip-top ');
|
|
content = content.replace(/(costaud[,\s]+){2,}/gi, 'costaud ');
|
|
|
|
// Nettoyer espaces multiples et retours ligne
|
|
content = content.replace(/\s{2,}/g, ' ');
|
|
content = content.replace(/\n{2,}/g, '\n');
|
|
content = content.trim();
|
|
|
|
return content;
|
|
}
|
|
|
|
// ============= PARSING FUNCTIONS =============
|
|
|
|
// FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel
|
|
|
|
// FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel
|
|
|
|
// Placeholder pour les fonctions de parsing conservées qui suivent
|
|
|
|
function parseTransitionsBatchResponse(response, chunk) {
|
|
const results = {};
|
|
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
|
|
let match;
|
|
let index = 0;
|
|
|
|
while ((match = regex.exec(response)) && index < chunk.length) {
|
|
let content = match[2].trim();
|
|
|
|
// Appliquer le nettoyage XML
|
|
content = cleanXMLTagsFromContent(content);
|
|
|
|
if (content && content.length > 10) {
|
|
results[chunk[index].tag] = content;
|
|
} else {
|
|
// Fallback si contenu invalide
|
|
results[chunk[index].tag] = chunk[index].content; // Garder contenu original
|
|
}
|
|
index++;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function parseStyleBatchResponse(response, chunk) {
|
|
const results = {};
|
|
const regex = /\[(\d+)\]\s*([^[]*?)(?=\n\[\d+\]|$)/gs;
|
|
let match;
|
|
let index = 0;
|
|
|
|
while ((match = regex.exec(response)) && index < chunk.length) {
|
|
let content = match[2].trim();
|
|
|
|
// Appliquer le nettoyage XML
|
|
content = cleanXMLTagsFromContent(content);
|
|
|
|
if (content && content.length > 10) {
|
|
results[chunk[index].tag] = content;
|
|
} else {
|
|
// Fallback si contenu invalide
|
|
results[chunk[index].tag] = chunk[index].content; // Garder contenu original
|
|
}
|
|
index++;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// ============= ANALYSIS FUNCTIONS =============
|
|
|
|
/**
|
|
* Analyser besoin d'amélioration transitions
|
|
*/
|
|
function analyzeTransitionNeed(content) {
|
|
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
|
|
|
|
// Critères multiples d'analyse
|
|
const metrics = {
|
|
repetitiveConnectors: analyzeRepetitiveConnectors(content),
|
|
abruptTransitions: analyzeAbruptTransitions(sentences),
|
|
sentenceVariety: analyzeSentenceVariety(sentences),
|
|
formalityLevel: analyzeFormalityLevel(content),
|
|
overallLength: content.length
|
|
};
|
|
|
|
// Score de besoin (0-1)
|
|
let needScore = 0;
|
|
needScore += metrics.repetitiveConnectors * 0.3;
|
|
needScore += metrics.abruptTransitions * 0.4;
|
|
needScore += (1 - metrics.sentenceVariety) * 0.2;
|
|
needScore += metrics.formalityLevel * 0.1;
|
|
|
|
// Seuil ajustable selon longueur
|
|
const threshold = metrics.overallLength > 300 ? 0.4 : 0.6;
|
|
|
|
logSh(`🔍 Analyse transitions: score=${needScore.toFixed(2)}, seuil=${threshold}`, 'DEBUG');
|
|
|
|
return needScore > threshold;
|
|
}
|
|
|
|
function analyzeRepetitiveConnectors(content) {
|
|
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc'];
|
|
let totalConnectors = 0;
|
|
let repetitions = 0;
|
|
|
|
connectors.forEach(connector => {
|
|
const matches = (content.match(new RegExp(`\\b${connector}\\b`, 'gi')) || []);
|
|
totalConnectors += matches.length;
|
|
if (matches.length > 1) repetitions += matches.length - 1;
|
|
});
|
|
|
|
return totalConnectors > 0 ? repetitions / totalConnectors : 0;
|
|
}
|
|
|
|
function analyzeAbruptTransitions(sentences) {
|
|
if (sentences.length < 2) return 0;
|
|
|
|
let abruptCount = 0;
|
|
|
|
for (let i = 1; i < sentences.length; i++) {
|
|
const current = sentences[i].trim();
|
|
const previous = sentences[i-1].trim();
|
|
|
|
const hasConnector = hasTransitionWord(current);
|
|
const topicContinuity = calculateTopicContinuity(previous, current);
|
|
|
|
// Transition abrupte = pas de connecteur + faible continuité thématique
|
|
if (!hasConnector && topicContinuity < 0.3) {
|
|
abruptCount++;
|
|
}
|
|
}
|
|
|
|
return abruptCount / (sentences.length - 1);
|
|
}
|
|
|
|
function analyzeSentenceVariety(sentences) {
|
|
if (sentences.length < 2) return 1;
|
|
|
|
const lengths = sentences.map(s => s.trim().length);
|
|
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
|
|
|
// Calculer variance des longueurs
|
|
const variance = lengths.reduce((acc, len) => acc + Math.pow(len - avgLength, 2), 0) / lengths.length;
|
|
const stdDev = Math.sqrt(variance);
|
|
|
|
// Score de variété (0-1) - plus la variance est élevée, plus c'est varié
|
|
return Math.min(1, stdDev / avgLength);
|
|
}
|
|
|
|
function analyzeFormalityLevel(content) {
|
|
const formalIndicators = [
|
|
'il convient de', 'par conséquent', 'néanmoins', 'toutefois',
|
|
'de surcroît', 'en définitive', 'il s\'avère que', 'force est de constater'
|
|
];
|
|
|
|
let formalCount = 0;
|
|
formalIndicators.forEach(indicator => {
|
|
if (content.toLowerCase().includes(indicator)) formalCount++;
|
|
});
|
|
|
|
const sentences = content.split(/[.!?]+/).length;
|
|
return sentences > 0 ? formalCount / sentences : 0;
|
|
}
|
|
|
|
function calculateTopicContinuity(sentence1, sentence2) {
|
|
const stopWords = ['les', 'des', 'une', 'sont', 'avec', 'pour', 'dans', 'cette', 'vous', 'peut', 'tout'];
|
|
|
|
const words1 = extractSignificantWords(sentence1, stopWords);
|
|
const words2 = extractSignificantWords(sentence2, stopWords);
|
|
|
|
if (words1.length === 0 || words2.length === 0) return 0;
|
|
|
|
const commonWords = words1.filter(word => words2.includes(word));
|
|
const semanticSimilarity = commonWords.length / Math.min(words1.length, words2.length);
|
|
|
|
const technicalWords = ['plaque', 'dibond', 'aluminium', 'impression', 'signalétique'];
|
|
const commonTechnical = commonWords.filter(word => technicalWords.includes(word));
|
|
const technicalBonus = commonTechnical.length * 0.2;
|
|
|
|
return Math.min(1, semanticSimilarity + technicalBonus);
|
|
}
|
|
|
|
function extractSignificantWords(sentence, stopWords) {
|
|
return sentence.toLowerCase()
|
|
.match(/\b[a-zàâäéèêëïîôùûüÿç]{4,}\b/g) // Mots 4+ lettres avec accents
|
|
?.filter(word => !stopWords.includes(word)) || [];
|
|
}
|
|
|
|
function hasTransitionWord(sentence) {
|
|
const connectors = ['par ailleurs', 'en effet', 'de plus', 'cependant', 'ainsi', 'donc', 'ensuite', 'puis', 'également', 'aussi', 'toutefois', 'néanmoins', 'alors', 'enfin'];
|
|
return connectors.some(connector => sentence.toLowerCase().includes(connector));
|
|
}
|
|
|
|
/**
|
|
* Instructions de style dynamiques
|
|
*/
|
|
function getPersonalityStyleInstructions(personality) {
|
|
// CORRECTION: Utilisation des VRAIS champs Google Sheets au lieu du hardcodé
|
|
if (!personality) return "Style professionnel standard";
|
|
|
|
const instructions = `STYLE ${personality.nom.toUpperCase()} (${personality.style}):
|
|
- Description: ${personality.description}
|
|
- Vocabulaire préféré: ${personality.vocabulairePref || 'professionnel, qualité'}
|
|
- Connecteurs préférés: ${personality.connecteursPref || 'par ailleurs, en effet'}
|
|
- Mots-clés secteurs: ${personality.motsClesSecteurs || 'technique, qualité'}
|
|
- Longueur phrases: ${personality.longueurPhrases || 'Moyennes (15-25 mots)'}
|
|
- Niveau technique: ${personality.niveauTechnique || 'Accessible'}
|
|
- Style CTA: ${personality.ctaStyle || 'Professionnel'}
|
|
- Défauts simulés: ${personality.defautsSimules || 'Aucun'}
|
|
- Erreurs typiques à éviter: ${personality.erreursTypiques || 'Répétitions, généralités'}`;
|
|
|
|
return instructions;
|
|
}
|
|
|
|
/**
|
|
* Créer prompt pour élément (fonction de base nécessaire)
|
|
*/
|
|
function createPromptForElement(element, csvData) {
|
|
const personality = csvData.personality;
|
|
const styleContext = `Rédige dans le style ${personality.style} de ${personality.nom} (${personality.description}).`;
|
|
|
|
switch (element.type) {
|
|
case 'titre_h1':
|
|
return `${styleContext}
|
|
MISSION: Crée un titre H1 accrocheur pour: ${csvData.mc0}
|
|
Référence: ${csvData.t0}
|
|
CONSIGNES: 10 mots maximum, direct et impactant, optimisé SEO.
|
|
RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`;
|
|
|
|
case 'titre_h2':
|
|
return `${styleContext}
|
|
MISSION: Crée un titre H2 optimisé SEO pour: ${csvData.mc0}
|
|
CONSIGNES: Intègre naturellement le mot-clé, 8 mots maximum.
|
|
RÉPONDS UNIQUEMENT PAR LE TITRE, sans introduction.`;
|
|
|
|
case 'intro':
|
|
if (element.instructions) {
|
|
return `${styleContext}
|
|
MISSION: ${element.instructions}
|
|
Données contextuelles:
|
|
- MC0: ${csvData.mc0}
|
|
- T-1: ${csvData.tMinus1}
|
|
- L-1: ${csvData.lMinus1}
|
|
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
|
|
}
|
|
return `${styleContext}
|
|
MISSION: Rédige une introduction de 100 mots pour ${csvData.mc0}.
|
|
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
|
|
|
|
case 'texte':
|
|
if (element.instructions) {
|
|
return `${styleContext}
|
|
MISSION: ${element.instructions}
|
|
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
|
|
}
|
|
return `${styleContext}
|
|
MISSION: Rédige un paragraphe de 150 mots sur ${csvData.mc0}.
|
|
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
|
|
|
|
case 'faq_question':
|
|
if (element.instructions) {
|
|
return `${styleContext}
|
|
MISSION: ${element.instructions}
|
|
CONTEXTE: ${csvData.mc0} - ${csvData.t0}
|
|
STYLE: Question ${csvData.personality?.style} de ${csvData.personality?.nom}
|
|
CONSIGNES:
|
|
- Vraie question que se poserait un client intéressé par ${csvData.mc0}
|
|
- Commence par "Comment", "Quel", "Pourquoi", "Où", "Quand" ou "Est-ce que"
|
|
- Maximum 15 mots, pratique et concrète
|
|
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'}
|
|
RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`;
|
|
}
|
|
return `${styleContext}
|
|
MISSION: Génère une vraie question FAQ client sur ${csvData.mc0}.
|
|
CONSIGNES:
|
|
- Question pratique et concrète qu'un client se poserait
|
|
- Commence par "Comment", "Quel", "Pourquoi", "Combien", "Où" ou "Est-ce que"
|
|
- Maximum 15 mots, style ${csvData.personality?.style}
|
|
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'accessible'}
|
|
RÉPONDS UNIQUEMENT PAR LA QUESTION, sans guillemets ni introduction.`;
|
|
|
|
case 'faq_reponse':
|
|
if (element.instructions) {
|
|
return `${styleContext}
|
|
MISSION: ${element.instructions}
|
|
CONTEXTE: ${csvData.mc0} - ${csvData.t0}
|
|
STYLE: Réponse ${csvData.personality?.style} de ${csvData.personality?.nom}
|
|
CONSIGNES:
|
|
- Réponse utile et rassurante
|
|
- 50-80 mots, ton ${csvData.personality?.style}
|
|
- Vocabulaire: ${csvData.personality?.vocabulairePref}
|
|
- Connecteurs: ${csvData.personality?.connecteursPref}
|
|
RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`;
|
|
}
|
|
return `${styleContext}
|
|
MISSION: Réponds à une question client sur ${csvData.mc0}.
|
|
CONSIGNES:
|
|
- Réponse utile, claire et rassurante
|
|
- 50-80 mots, ton ${csvData.personality?.style} de ${csvData.personality?.nom}
|
|
- Vocabulaire: ${csvData.personality?.vocabulairePref || 'professionnel'}
|
|
- Connecteurs: ${csvData.personality?.connecteursPref || 'par ailleurs'}
|
|
RÉPONDS UNIQUEMENT PAR LA RÉPONSE, sans introduction.`;
|
|
|
|
default:
|
|
return `${styleContext}
|
|
MISSION: Génère du contenu pertinent pour ${csvData.mc0}.
|
|
RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
|
|
*/
|
|
async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
|
|
const contentEntries = Object.keys(baseContents);
|
|
|
|
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
|
|
|
|
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
|
|
|
|
CONTENUS À ANALYSER:
|
|
|
|
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
|
|
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
|
|
|
|
CONSIGNES:
|
|
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
|
|
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
|
|
- Focus: matériaux, procédés, normes, dimensions, technologies
|
|
- Si aucun terme technique → "AUCUN"
|
|
|
|
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
|
|
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
|
|
|
|
FORMAT RÉPONSE EXACT:
|
|
[1] dibond, impression UV, 3mm OU AUCUN
|
|
[2] aluminium, fraisage CNC OU AUCUN
|
|
[3] AUCUN
|
|
etc... (${contentEntries.length} lignes total)`;
|
|
|
|
try {
|
|
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
|
|
temperature: 0.3,
|
|
maxTokens: 2000
|
|
}, csvData.personality);
|
|
|
|
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
|
|
|
|
} catch (error) {
|
|
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
|
|
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
|
*/
|
|
async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
|
|
if (elementsNeedingEnhancement.length === 0) return {};
|
|
|
|
const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus.
|
|
|
|
CONTEXTE: Article SEO pour site e-commerce de signalétique
|
|
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} web professionnel)
|
|
SUJET: ${csvData.mc0} - Secteur: Signalétique/impression
|
|
VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
|
|
|
|
CONTENUS + TERMES À AMÉLIORER:
|
|
|
|
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
|
CONTENU ACTUEL: "${item.content}"
|
|
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
|
|
|
|
CONSIGNES STRICTES:
|
|
- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom}
|
|
- GARDE la même longueur, structure et ton
|
|
- Intègre naturellement les termes techniques listés
|
|
- NE CHANGE PAS le fond du message ni le style personnel
|
|
- Utilise un vocabulaire expert mais accessible
|
|
- ÉVITE les répétitions excessives
|
|
- RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique}
|
|
- Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA
|
|
|
|
FORMAT RÉPONSE:
|
|
[1] Contenu avec amélioration technique selon ${csvData.personality?.nom}
|
|
[2] Contenu avec amélioration technique selon ${csvData.personality?.nom}
|
|
etc... (${elementsNeedingEnhancement.length} éléments total)`;
|
|
|
|
try {
|
|
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
|
|
temperature: 0.4,
|
|
maxTokens: 5000 // Plus large pour batch total
|
|
}, csvData.personality);
|
|
|
|
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
|
|
|
|
} catch (error) {
|
|
logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
|
|
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parser réponse extraction termes
|
|
*/
|
|
function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
|
|
const results = [];
|
|
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
|
let match;
|
|
const parsedItems = {};
|
|
|
|
// Parser la réponse
|
|
while ((match = regex.exec(response)) !== null) {
|
|
const index = parseInt(match[1]) - 1; // Convertir en 0-indexé
|
|
const termsText = match[2].trim();
|
|
parsedItems[index] = termsText;
|
|
}
|
|
|
|
// Mapper aux éléments
|
|
contentEntries.forEach((tag, index) => {
|
|
const termsText = parsedItems[index] || 'AUCUN';
|
|
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
|
|
|
|
const technicalTerms = hasTerms ?
|
|
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
|
|
[];
|
|
|
|
results.push({
|
|
tag: tag,
|
|
content: baseContents[tag],
|
|
technicalTerms: technicalTerms,
|
|
needsEnhancement: hasTerms && technicalTerms.length > 0
|
|
});
|
|
|
|
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG');
|
|
});
|
|
|
|
const enhancementCount = results.filter(r => r.needsEnhancement).length;
|
|
logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO');
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Parser réponse enhancement technique
|
|
*/
|
|
function parseTechnicalEnhancementBatchResponse(response, elementsNeedingEnhancement) {
|
|
const results = {};
|
|
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
|
let match;
|
|
let index = 0;
|
|
|
|
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
|
|
let content = match[2].trim();
|
|
const element = elementsNeedingEnhancement[index];
|
|
|
|
// NOUVEAU: Appliquer le nettoyage XML
|
|
content = cleanXMLTagsFromContent(content);
|
|
|
|
if (content && content.length > 10) {
|
|
results[element.tag] = content;
|
|
logSh(`✅ Enhanced [${element.tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
|
|
} else {
|
|
// Fallback si contenu invalide après nettoyage
|
|
results[element.tag] = element.content;
|
|
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide après nettoyage`, 'WARNING');
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
// Vérifier si on a bien tout parsé
|
|
if (Object.keys(results).length < elementsNeedingEnhancement.length) {
|
|
logSh(`⚠️ Parsing partiel: ${Object.keys(results).length}/${elementsNeedingEnhancement.length}`, 'WARNING');
|
|
|
|
// Compléter avec contenu original pour les manquants
|
|
elementsNeedingEnhancement.forEach(element => {
|
|
if (!results[element.tag]) {
|
|
results[element.tag] = element.content;
|
|
}
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// ============= EXPORTS =============
|
|
|
|
module.exports = {
|
|
generateWithBatchEnhancement,
|
|
generateAllContentBase,
|
|
enhanceAllTechnicalTerms,
|
|
enhanceAllTransitions,
|
|
enhanceAllPersonalityStyle,
|
|
collectAllElements,
|
|
groupElementsByType,
|
|
chunkArray,
|
|
createBatchBasePrompt,
|
|
parseBatchResponse,
|
|
cleanXMLTagsFromContent,
|
|
analyzeTransitionNeed,
|
|
getPersonalityStyleInstructions,
|
|
createPromptForElement,
|
|
sleep,
|
|
separateFAQPairsAndOthers,
|
|
generateFAQPairsRestored,
|
|
createBatchFAQPairsPrompt,
|
|
parseFAQPairsResponse,
|
|
cleanFAQInstructions,
|
|
extractAllTechnicalTermsBatch,
|
|
enhanceAllElementsTechnicalBatch,
|
|
parseAllTechnicalTermsResponse,
|
|
parseTechnicalEnhancementBatchResponse
|
|
}; |