diff --git a/.gitignore b/.gitignore index cff1b08..7217f01 100644 --- a/.gitignore +++ b/.gitignore @@ -53,8 +53,7 @@ test_*.js *_debug.js test-*.js  -# HTML généré (logs viewer) -logs-viewer.html +# HTML généré était ici mais maintenant on le garde dans tools/  # Unit test reports TEST*.xml diff --git a/code.js b/code.js index a1dcd72..a2bdb22 100644 --- a/code.js +++ b/code.js @@ -1,6 +1,6 @@ /* code.js — bundle concaténé - Généré: 2025-09-03T04:21:57.159Z + Généré: 2025-09-04T01:10:08.540Z Source: lib Fichiers: 16 Ordre: topo @@ -57,12 +57,13 @@ const fileDest = pino.destination({ }); tee.pipe(fileDest);  -// Custom levels for Pino to include TRACE and PROMPT +// Custom levels for Pino to include TRACE, PROMPT, and LLM const customLevels = { trace: 5, // Below debug (10) debug: 10, info: 20, prompt: 25, // New level for prompts (between info and warn) + llm: 26, // New level for LLM interactions (between prompt and warn) warn: 30, error: 40, fatal: 50 @@ -178,6 +179,9 @@ async function logSh(message, level = 'INFO') { case 'prompt': logger.prompt(traceData, message); break; + case 'llm': + logger.llm(traceData, message); + break; default: logger.info(traceData, message); } @@ -1282,7 +1286,10 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) { // 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT'); logSh(prompt, 'PROMPT'); - logSh(`===== FIN PROMPT ${llmProvider.toUpperCase()} (${personality?.nom || 'AUCUNE'}) =====\n`, 'PROMPT'); +  + // 📤 LOG LLM REQUEST COMPLET + logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM'); + logSh(prompt, 'LLM');  // Préparer la requête selon le provider const requestData = buildRequestData(llmProvider, prompt, options, personality); @@ -1293,8 +1300,12 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) { // Parser la réponse selon le format du provider const content = parseResponse(llmProvider, response);  + // 📥 LOG LLM RESPONSE COMPLET + logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM'); + logSh(content, 'LLM'); +  const duration = Date.now() - startTime; - logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms: "${content.substring(0, 150)}${content.length > 150 ? '...' : ''}"`, 'INFO'); + logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');  // Enregistrer les stats d'usage await recordUsageStats(llmProvider, prompt.length, content.length, duration); @@ -1727,6 +1738,8 @@ module.exports = { LLM_CONFIG };  + + /* ┌────────────────────────────────────────────────────────────────────┐ │ File: lib/ElementExtraction.js │ @@ -2309,38 +2322,21 @@ CONTEXTE: `;  missingElements.forEach((missing, index) => { - prompt += `${index + 1}. [${missing.name}] `; -  - // INSTRUCTIONS SPÉCIFIQUES PAR TYPE - if (missing.type.includes('titre_h1')) { - prompt += `→ Titre H1 principal (8-10 mots) pour ${contextAnalysis.mainKeyword}\n`; - } else if (missing.type.includes('titre_h2')) { - prompt += `→ Titre H2 section (6-8 mots) lié à ${contextAnalysis.mainKeyword}\n`; - } else if (missing.type.includes('titre_h3')) { - prompt += `→ Sous-titre H3 (4-6 mots) spécialisé ${contextAnalysis.mainKeyword}\n`; - } else if (missing.type.includes('texte') || missing.type.includes('txt')) { - prompt += `→ Thème/sujet pour paragraphe 150 mots sur ${contextAnalysis.mainKeyword}\n`; - } else if (missing.type.includes('faq_question')) { - prompt += `→ Question client directe sur ${contextAnalysis.mainKeyword} (8-12 mots)\n`; - } else if (missing.type.includes('faq_reponse')) { - prompt += `→ Thème réponse experte ${contextAnalysis.mainKeyword} (2-4 mots)\n`; - } else { - prompt += `→ Expression/mot-clé pertinent ${contextAnalysis.mainKeyword}\n`; - } + prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`; });  prompt += `\nCONSIGNES: -- Reste dans le thème ${contextAnalysis.mainKeyword} -- Varie les angles et expressions -- Évite répétitions avec mots-clés existants -- Précis et pertinents +- Thème: ${contextAnalysis.mainKeyword} +- Mots-clés SEO naturels +- Varie les termes +- Évite répétitions  FORMAT: [${missingElements[0].name}] -Expression/mot-clé généré 1 +mot-clé  [${missingElements[1] ? missingElements[1].name : 'exemple'}] -Expression/mot-clé généré 2 +mot-clé  etc...`;  @@ -2596,6 +2592,11 @@ 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 @@ -2804,8 +2805,8 @@ async function generateAllContentBase(hierarchy, csvData, aiProvider) { }  /** - * ÉTAPE 2 - Enhancement technique BATCH OPTIMISÉ avec IA configurable - * OPTIMISATION : 1 appel extraction + 1 appel enhancement au lieu de 20+ + * É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'); @@ -2872,96 +2873,96 @@ async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) { }  /** - * NOUVELLE FONCTION : Extraction batch TOUS les termes techniques + * Analyser un seul élément pour détecter 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. +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  -CONTENUS À ANALYSER: - -${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag} -CONTENU: "${baseContents[tag]}"`).join('\n\n')} +CONTENU À ANALYSER: +TAG: ${tag} +CONTENU: "${content}"  CONSIGNES: -- Identifie UNIQUEMENT les vrais termes techniques métier/industrie +- 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 -- Si aucun terme technique → "AUCUN" +- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques  -EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé -EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique +EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation +EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance  -FORMAT RÉPONSE EXACT: -[1] dibond, impression UV, 3mm OU AUCUN -[2] aluminium, fraisage CNC OU AUCUN -[3] AUCUN -etc... (${contentEntries.length} lignes total)`; +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 analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, { - temperature: 0.3, - maxTokens: 2000 - }, csvData.personality); -  - return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries); -  + 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(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR'); - throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`); + logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR'); + return false; // En cas d'erreur, on skip l'enhancement } }  /** - * NOUVELLE FONCTION : Enhancement batch TOUS les éléments + * Enhancer un seul élément techniquement */ -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. +async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) { + const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.  -PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style}) -CONTEXTE: ${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')} +CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression  -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 +CONTENU À AMÉLIORER: +TAG: ${tag} +CONTENU: "${content}"  -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)`; +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 enhanced = await callLLM(aiProvider, batchEnhancementPrompt, { - temperature: 0.4, - maxTokens: 5000 // Plus large pour batch total - }, csvData.personality); -  - return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement); -  + const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 }); + return enhancedContent.trim(); } 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}`); + 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 */ @@ -3015,7 +3016,8 @@ async function enhanceAllTransitions(baseContents, csvData, aiProvider) {  const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.  -PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style}) +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: @@ -3093,9 +3095,10 @@ async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {  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} +STYLE CIBLE: ${personality.style} adapté au web professionnel VOCABULAIRE: ${personality.vocabulairePref} CONNECTEURS: ${personality.connecteursPref} NIVEAU TECHNIQUE: ${personality.niveauTechnique} @@ -3149,9 +3152,8 @@ etc...`; /** * Sleep function replacement for Utilities.sleep */ -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} + +// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12  /** * RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes @@ -3187,32 +3189,33 @@ async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) { function createBatchFAQPairsPrompt(faqPairs, csvData) { const personality = csvData.personality;  - let prompt = `PERSONNALITÉ: ${personality.nom} | ${personality.description} -STYLE: ${personality.style} -VOCABULAIRE: ${personality.vocabulairePref} -CONNECTEURS: ${personality.connecteursPref} -NIVEAU TECHNIQUE: ${personality.niveauTechnique} + let prompt = `=== 1. CONTEXTE === +Entreprise: Autocollant.fr - signalétique personnalisée +Sujet: ${csvData.mc0} +Section: FAQ pour article SEO commercial  -GÉNÈRE ${faqPairs.length} PAIRES FAQ COHÉRENTES pour ${csvData.mc0}: +=== 2. PERSONNALITÉ === +Rédacteur: ${personality.nom} +Style: ${personality.style} +Ton: ${personality.description || 'professionnel'}  -RÈGLES STRICTES: -- QUESTIONS: Neutres, directes, langage client naturel (8-15 mots) -- RÉPONSES: Style ${personality.style}, vocabulaire ${personality.vocabulairePref} (50-80 mots) -- Sujets à couvrir: prix, livraison, personnalisation, installation, durabilité -- ÉVITE répétitions excessives et expressions trop familières -- Style ${personality.nom} reconnaissable mais PROFESSIONNEL -- PAS de messages d'excuse ("je n'ai pas l'information") -- RÉPONDS DIRECTEMENT par questions et réponses, sans préfixe +=== 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 ===  -PAIRES À 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}] - Question client sur ${csvData.mc0} → Réponse ${personality.style} + prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle `; });  @@ -3533,10 +3536,25 @@ function findAssociatedTitle(textElement, existingResults) { function createBatchBasePrompt(elements, type, csvData, existingResults = {}) { const personality = csvData.personality;  - let prompt = `RÉDACTEUR: ${personality.nom} | Style: ${personality.style} -SUJET: ${csvData.mc0} - -${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${elements.length} ${type.toUpperCase()}S PROFESSIONNELS`}: + 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 @@ -3548,7 +3566,7 @@ ${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${el  if (generatedTitles.length > 0) { prompt += ` -CONTEXTE - TITRES GÉNÉRÉS: +Titres existants pour contexte: ${generatedTitles.join('\n')}  `; @@ -3560,34 +3578,33 @@ ${generatedTitles.join('\n')}  prompt += `${index + 1}. [${cleanTag}] `;  - // INSTRUCTIONS SPÉCIFIQUES ET COURTES PAR TYPE + // INSTRUCTIONS PROPRES PAR ÉLÉMENT if (type === 'titre') { if (elementInfo.element.type === 'titre_h1') { - prompt += `CRÉER UN TITRE H1 PRINCIPAL (8-12 mots) sur "${csvData.t0}" - NE PAS écrire "Titre_H1_1"\n`; + prompt += `Titre principal accrocheur\n`; } else if (elementInfo.element.type === 'titre_h2') { - prompt += `CRÉER UN TITRE H2 SECTION (6-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_H2_X"\n`; + prompt += `Titre de section engageant\n`; } else if (elementInfo.element.type === 'titre_h3') { - prompt += `CRÉER UN TITRE H3 SOUS-SECTION (4-8 mots) - NE PAS écrire "Titre_H3_X"\n`; + prompt += `Sous-titre spécialisé\n`; } else { - prompt += `CRÉER UN TITRE ACCROCHEUR (4-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_"\n`; + prompt += `Titre pertinent\n`; } } else if (type === 'texte') { - const wordCount = elementInfo.element.name && elementInfo.element.name.includes('H2') ? '150' : '100'; - prompt += `Paragraphe ${wordCount} mots, style ${personality.style}\n`; + prompt += `Paragraphe informatif\n`;  // ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT const associatedTitle = findAssociatedTitle(elementInfo, existingResults); if (associatedTitle) { - prompt += ` Développe le titre: "${associatedTitle}"\n`; + prompt += ` Contexte: "${associatedTitle}"\n`; }  if (elementInfo.element.resolvedContent) { - prompt += ` Thème: "${elementInfo.element.resolvedContent}"\n`; + prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`; } } else if (type === 'intro') { - prompt += `Introduction 80-100 mots, ton accueillant\n`; + prompt += `Introduction engageante\n`; } else { - prompt += `Contenu pertinent pour ${csvData.mc0}\n`; + prompt += `Contenu pertinent\n`; } });  @@ -3597,15 +3614,16 @@ ${generatedTitles.join('\n')} - Phrases: ${personality.longueurPhrases} - Niveau technique: ${personality.niveauTechnique}  -CONSIGNES STRICTES: -- RESPECTE le style ${personality.style} de ${personality.nom} mais RESTE PROFESSIONNEL -- INTERDICTION ABSOLUE: "du coup", "bon", "alors", "franchement", "nickel", "tip-top", "costaud" en excès -- VARIE les connecteurs: ${personality.connecteursPref} -- POUR LES TITRES: SEULEMENT le titre réel, JAMAIS de référence "Titre_H1_1" ou "Titre_H2_7" -- EXEMPLE TITRE: "Plaques personnalisées résistantes aux intempéries" PAS "Titre_H2_1"  -- RÉPONDS DIRECTEMENT par le contenu demandé, SANS introduction ni nom de tag -- PAS de message d'excuse du type "je n'ai pas l'information" -- CONTENU cohérent et professionnel, évite la sur-familiarité +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, '')}] @@ -3696,107 +3714,11 @@ function cleanXMLTagsFromContent(content) {  // ============= PARSING FUNCTIONS =============  -/** - * 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; [31