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} +CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression -CONTENUS + TERMES À AMÉLIORER: +CONTENU À AMÉLIORER: +TAG: ${tag} +CONTENU: "${content}" -${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag} -CONTENU ACTUEL: "${item.content}" -TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')} +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 -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 +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" -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)`; +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} + let prompt = `=== 1. CONTEXTE === +Entreprise: Autocollant.fr - signalétique personnalisée +Sujet: ${csvData.mc0} +Type d'article: SEO professionnel pour site commercial -${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${elements.length} ${type.toUpperCase()}S PROFESSIONNELS`}: +=== 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; -} +// FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel -/** - * 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; -} +// FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel -/** - * Parser réponse batch transitions/style (format identique) - */ -function parseTechnicalBatchResponse(response, chunk) { - const results = {}; - const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; - let match; - let index = 0; - - while ((match = regex.exec(response)) && index < chunk.length) { - const content = match[2].trim(); - results[chunk[index].tag] = content; - index++; - } - - return results; -} +// Placeholder pour les fonctions de parsing conservées qui suivent function parseTransitionsBatchResponse(response, chunk) { const results = {}; @@ -4088,6 +4010,182 @@ 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 = { @@ -4110,7 +4208,11 @@ module.exports = { generateFAQPairsRestored, createBatchFAQPairsPrompt, parseFAQPairsResponse, - cleanFAQInstructions + cleanFAQInstructions, + extractAllTechnicalTermsBatch, + enhanceAllElementsTechnicalBatch, + parseAllTechnicalTermsResponse, + parseTechnicalEnhancementBatchResponse }; /* @@ -5107,6 +5209,31 @@ const SHEET_CONFIG = { keyFile: './seo-generator-470715-85d4a971c1af.json' }; +async function deployArticle({ path, html, dryRun = false, ...rest }) { + if (!path || typeof html !== 'string') { + const err = new Error('deployArticle: invalid payload (requires {path, html})'); + err.code = 'E_PAYLOAD'; + throw err; + } + if (dryRun) { + return { + ok: true, + dryRun: true, + length: html.length, + path, + meta: rest || {} + }; + } + // --- Impl réelle à toi ici (upload DO Spaces / API / SSH etc.) --- + // return await realDeploy({ path, html, ...rest }); + + // Placeholder pour ne pas casser l'appel si pas encore implémenté + return { ok: true, dryRun: false, path, length: html.length }; +} + +module.exports.deployArticle = module.exports.deployArticle || deployArticle; + + // ============= TRIGGER PRINCIPAL REMPLACÉ PAR WEBHOOK/API ============= /** @@ -5549,22 +5676,42 @@ let logViewerLaunched = false; * Lancer le log viewer dans Edge */ function launchLogViewer() { - if (logViewerLaunched) return; + if (logViewerLaunched || process.env.NODE_ENV === 'test') return; try { - const logViewerPath = path.join(__dirname, '..', 'logs-viewer.html'); + const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html'); const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`; - // Lancer Edge avec l'URL du fichier - const edgeProcess = spawn('cmd', ['/c', 'start', 'msedge', fileUrl], { - detached: true, - stdio: 'ignore' - }); + // Détecter l'environnement et adapter la commande + const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP; + const isWindows = process.platform === 'win32'; + + if (isWindows && !isWSL) { + // Windows natif + const edgeProcess = spawn('cmd', ['/c', 'start', 'msedge', fileUrl], { + detached: true, + stdio: 'ignore' + }); + edgeProcess.unref(); + } else if (isWSL) { + // WSL - utiliser cmd.exe via /mnt/c/Windows/System32/ + const edgeProcess = spawn('/mnt/c/Windows/System32/cmd.exe', ['/c', 'start', 'msedge', fileUrl], { + detached: true, + stdio: 'ignore' + }); + edgeProcess.unref(); + } else { + // Linux/Mac - essayer xdg-open ou open + const command = process.platform === 'darwin' ? 'open' : 'xdg-open'; + const browserProcess = spawn(command, [fileUrl], { + detached: true, + stdio: 'ignore' + }); + browserProcess.unref(); + } - edgeProcess.unref(); logViewerLaunched = true; - - logSh('🌐 Log viewer ouvert dans Edge', 'INFO'); + logSh('🌐 Log viewer lancé', 'INFO'); } catch (error) { logSh(`⚠️ Impossible d'ouvrir le log viewer: ${error.message}`, 'WARNING'); } diff --git a/lib/ErrorReporting.js b/lib/ErrorReporting.js index f414c6b..dcfe1eb 100644 --- a/lib/ErrorReporting.js +++ b/lib/ErrorReporting.js @@ -43,12 +43,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 @@ -164,6 +165,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); } diff --git a/lib/LLMManager.js b/lib/LLMManager.js index de26435..364931a 100644 --- a/lib/LLMManager.js +++ b/lib/LLMManager.js @@ -125,7 +125,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); @@ -136,8 +139,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); diff --git a/lib/Main.js b/lib/Main.js index 28f933d..fc8452f 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -30,7 +30,7 @@ function launchLogViewer() { if (logViewerLaunched || process.env.NODE_ENV === 'test') return; try { - const logViewerPath = path.join(__dirname, '..', 'logs-viewer.html'); + const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html'); const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`; // Détecter l'environnement et adapter la commande diff --git a/lib/SelectiveEnhancement.js b/lib/SelectiveEnhancement.js index 7edab91..df43fc0 100644 --- a/lib/SelectiveEnhancement.js +++ b/lib/SelectiveEnhancement.js @@ -8,6 +8,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 @@ -216,8 +221,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'); @@ -284,97 +289,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. -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} +CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression -CONTENUS + TERMES À AMÉLIORER: +CONTENU À AMÉLIORER: +TAG: ${tag} +CONTENU: "${content}" -${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag} -CONTENU ACTUEL: "${item.content}" -TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')} +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 -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 +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" -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)`; +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 */ @@ -564,9 +568,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 @@ -1127,107 +1130,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; -} +// FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel -/** - * 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; -} +// FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel -/** - * Parser réponse batch transitions/style (format identique) - */ -function parseTechnicalBatchResponse(response, chunk) { - const results = {}; - const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs; - let match; - let index = 0; - - while ((match = regex.exec(response)) && index < chunk.length) { - const content = match[2].trim(); - results[chunk[index].tag] = content; - index++; - } - - return results; -} +// Placeholder pour les fonctions de parsing conservées qui suivent function parseTransitionsBatchResponse(response, chunk) { const results = {}; @@ -1519,6 +1426,182 @@ 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 = { @@ -1541,5 +1624,9 @@ module.exports = { generateFAQPairsRestored, createBatchFAQPairsPrompt, parseFAQPairsResponse, - cleanFAQInstructions + cleanFAQInstructions, + extractAllTechnicalTermsBatch, + enhanceAllElementsTechnicalBatch, + parseAllTechnicalTermsResponse, + parseTechnicalEnhancementBatchResponse }; \ No newline at end of file diff --git a/tools/log-server.cjs b/tools/log-server.cjs new file mode 100644 index 0000000..d950ad2 --- /dev/null +++ b/tools/log-server.cjs @@ -0,0 +1,179 @@ +#!/usr/bin/env node +// tools/log-server.js - Serveur simple pour visualiser les logs +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const { exec } = require('child_process'); + +const app = express(); +const PORT = 3001; + +// Servir les fichiers statiques depuis la racine du projet +app.use(express.static(path.join(__dirname, '..'))); + +// Route pour servir les fichiers de log +app.use('/logs', express.static(path.join(__dirname, '..', 'logs'))); + +// Liste des fichiers de log disponibles +app.get('/api/logs', (req, res) => { + try { + const logsDir = path.join(__dirname, '..', 'logs'); + const files = fs.readdirSync(logsDir) + .filter(file => file.endsWith('.log')) + .map(file => { + const filePath = path.join(logsDir, file); + const stats = fs.statSync(filePath); + return { + name: file, + size: stats.size, + modified: stats.mtime.toISOString(), + url: `http://localhost:${PORT}/tools/logs-viewer.html?file=${file}` + }; + }) + .sort((a, b) => new Date(b.modified) - new Date(a.modified)); + + res.json({ files }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Page d'accueil avec liste des logs +app.get('/', (req, res) => { + res.send(` + + + + Log Viewer Server + + + +

📊 SEO Generator - Log Viewer

+ + 🔴 Logs en temps réel + +
+

Fichiers de log disponibles

+
Chargement...
+
+ + + + + `); +}); + +// Fonction pour ouvrir automatiquement le dernier log +function openLatestLog() { + try { + const logsDir = path.join(__dirname, '..', 'logs'); + const files = fs.readdirSync(logsDir) + .filter(file => file.endsWith('.log')) + .map(file => { + const filePath = path.join(logsDir, file); + const stats = fs.statSync(filePath); + return { + name: file, + modified: stats.mtime + }; + }) + .sort((a, b) => b.modified - a.modified); + + if (files.length > 0) { + const latestFile = files[0].name; + const url = `http://localhost:${PORT}/tools/logs-viewer.html?file=${latestFile}`; + + // Ouvrir dans le navigateur par défaut + // Utiliser powershell Start-Process pour ouvrir l'URL dans le navigateur + const command = 'powershell.exe Start-Process'; + + exec(`${command} "${url}"`, (error) => { + if (error) { + console.log(`⚠️ Impossible d'ouvrir automatiquement: ${error.message}`); + console.log(`🌐 Ouvrez manuellement: ${url}`); + } else { + console.log(`🌐 Ouverture automatique du dernier log: ${latestFile}`); + } + }); + } else { + console.log(`📊 Aucun log disponible - accédez à http://localhost:${PORT}/tools/logs-viewer.html`); + } + } catch (error) { + console.log(`⚠️ Erreur lors de l'ouverture: ${error.message}`); + } +} + +app.listen(PORT, () => { + console.log(`🚀 Log server running at http://localhost:${PORT}`); + console.log(`📊 Logs viewer: http://localhost:${PORT}/tools/logs-viewer.html`); + console.log(`📁 Logs directory: ${path.join(__dirname, '..', 'logs')}`); + + // Attendre un peu que le serveur soit prêt, puis ouvrir le navigateur + setTimeout(openLatestLog, 1000); +}); \ No newline at end of file diff --git a/tools/logs-viewer.html b/tools/logs-viewer.html new file mode 100644 index 0000000..9aae9ae --- /dev/null +++ b/tools/logs-viewer.html @@ -0,0 +1,921 @@ + + + + + + SEO Generator - Logs en temps réel + + + +
+
+

SEO Generator - Logs temps réel

+ Connexion... + Port: 8081 +
+ + +
+
+
+ Filtres: + + + + + + + +
+ + + +
+
+ +
+ +
0 résultats
+ + + +
+ +
+
+ --:--:-- + INFO + En attente des logs... +
+
+ + + + \ No newline at end of file diff --git a/tools/logviewer.cjs b/tools/logviewer.cjs index e0a4f0c..7571ef4 100644 --- a/tools/logviewer.cjs +++ b/tools/logviewer.cjs @@ -26,7 +26,7 @@ function setLogFile(filePath) { LOG_FILE = path.resolve(process.cwd(), filePath) function MB(n){return n*1024*1024;} function toInt(v,d){const n=parseInt(v,10);return Number.isFinite(n)?n:d;} -const LEVEL_MAP_NUM = {10:'TRACE',20:'DEBUG',25:'PROMPT',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'}; +const LEVEL_MAP_NUM = {10:'TRACE',20:'DEBUG',25:'PROMPT',26:'LLM',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'}; function normLevel(v){ if (v==null) return 'UNKNOWN'; if (typeof v==='number') return LEVEL_MAP_NUM[v]||String(v);