Add logviewer.html, prompt improvment, clean duplication

This commit is contained in:
Trouve Alexis 2025-09-04 09:27:07 +08:00
parent 31ea27153d
commit ad9e3e1374
9 changed files with 1765 additions and 421 deletions

3
.gitignore vendored
View File

@ -53,8 +53,7 @@ test_*.js
*_debug.js *_debug.js
test-*.js test-*.js
# HTML généré (logs viewer) # HTML généré était ici mais maintenant on le garde dans tools/
logs-viewer.html
# Unit test reports # Unit test reports
TEST*.xml TEST*.xml

641
code.js
View File

@ -1,6 +1,6 @@
/* /*
code.js bundle concaténé 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 Source: lib
Fichiers: 16 Fichiers: 16
Ordre: topo Ordre: topo
@ -57,12 +57,13 @@ const fileDest = pino.destination({
}); });
tee.pipe(fileDest); tee.pipe(fileDest);
// Custom levels for Pino to include TRACE and PROMPT // Custom levels for Pino to include TRACE, PROMPT, and LLM
const customLevels = { const customLevels = {
trace: 5, // Below debug (10) trace: 5, // Below debug (10)
debug: 10, debug: 10,
info: 20, info: 20,
prompt: 25, // New level for prompts (between info and warn) prompt: 25, // New level for prompts (between info and warn)
llm: 26, // New level for LLM interactions (between prompt and warn)
warn: 30, warn: 30,
error: 40, error: 40,
fatal: 50 fatal: 50
@ -178,6 +179,9 @@ async function logSh(message, level = 'INFO') {
case 'prompt': case 'prompt':
logger.prompt(traceData, message); logger.prompt(traceData, message);
break; break;
case 'llm':
logger.llm(traceData, message);
break;
default: default:
logger.info(traceData, message); logger.info(traceData, message);
} }
@ -1282,7 +1286,10 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA // 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT'); logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
logSh(prompt, '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 // Préparer la requête selon le provider
const requestData = buildRequestData(llmProvider, prompt, options, personality); 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 // Parser la réponse selon le format du provider
const content = parseResponse(llmProvider, response); 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; 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 // Enregistrer les stats d'usage
await recordUsageStats(llmProvider, prompt.length, content.length, duration); await recordUsageStats(llmProvider, prompt.length, content.length, duration);
@ -1727,6 +1738,8 @@ module.exports = {
LLM_CONFIG LLM_CONFIG
}; };
/* /*
File: lib/ElementExtraction.js File: lib/ElementExtraction.js
@ -2309,38 +2322,21 @@ CONTEXTE:
`; `;
missingElements.forEach((missing, index) => { missingElements.forEach((missing, index) => {
prompt += `${index + 1}. [${missing.name}] `; prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`;
// 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 += `\nCONSIGNES: prompt += `\nCONSIGNES:
- Reste dans le thème ${contextAnalysis.mainKeyword} - Thème: ${contextAnalysis.mainKeyword}
- Varie les angles et expressions - Mots-clés SEO naturels
- Évite répétitions avec mots-clés existants - Varie les termes
- Précis et pertinents - Évite répétitions
FORMAT: FORMAT:
[${missingElements[0].name}] [${missingElements[0].name}]
Expression/mot-clé généré 1 mot-clé
[${missingElements[1] ? missingElements[1].name : 'exemple'}] [${missingElements[1] ? missingElements[1].name : 'exemple'}]
Expression/mot-clé généré 2 mot-clé
etc...`; etc...`;
@ -2596,6 +2592,11 @@ const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace.js'); const { tracer } = require('./trace.js');
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig'); 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 * NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection * 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 * ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
* OPTIMISATION : 1 appel extraction + 1 appel enhancement au lieu de 20+ * NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
*/ */
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) { async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO'); 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) { async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
const contentEntries = Object.keys(baseContents); const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER: CONTENU À ANALYSER:
TAG: ${tag}
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag} CONTENU: "${content}"
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
CONSIGNES: 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.) - Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
- Focus: matériaux, procédés, normes, dimensions, technologies - Focus: matériaux, procédés, normes, dimensions, technologies spécifiques
- Si aucun terme technique "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance
FORMAT RÉPONSE EXACT: RÉPONSE REQUISE:
[1] dibond, impression UV, 3mm OU AUCUN - Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
[2] aluminium, fraisage CNC OU AUCUN - Si aucun terme technique: "NON"
[3] AUCUN
etc... (${contentEntries.length} lignes total)`; EXEMPLE:
OUI - termes: aluminium composite, impression numérique, gravure laser`;
try { try {
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, { const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
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) { } catch (error) {
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR'); logSh(`ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`); 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) { async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
if (elementsNeedingEnhancement.length === 0) return {}; const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.
const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus.
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style}) CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTEXTE: ${csvData.mc0} - Secteur: Signalétique/impression
VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
CONTENUS + TERMES À AMÉLIORER: CONTENU À AMÉLIORER:
TAG: ${tag}
CONTENU: "${content}"
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag} OBJECTIFS:
CONTENU ACTUEL: "${item.content}" - Remplace les termes génériques par des termes techniques précis
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')} - 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: EXEMPLE DE TRANSFORMATION:
- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom} "matériaux haute performance" "dibond 3mm ou aluminium composite"
- GARDE la même longueur, structure et ton "impression moderne" "impression UV haute définition"
- Intègre naturellement les termes techniques listés "fixation solide" "fixation par chevilles inox Ø6mm"
- 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: CONTRAINTES:
[1] Contenu avec amélioration technique selon ${csvData.personality?.nom} - GARDE la même structure
[2] Contenu avec amélioration technique selon ${csvData.personality?.nom} - MÊME longueur approximative
etc... (${elementsNeedingEnhancement.length} éléments total)`; - Style cohérent avec l'original
- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;
try { try {
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, { const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
temperature: 0.4, return enhancedContent.trim();
maxTokens: 5000 // Plus large pour batch total
}, csvData.personality);
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
} catch (error) { } catch (error) {
logSh(`FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR'); logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`); 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 * É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. 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} CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref}
CONTENUS: CONTENUS:
@ -3093,9 +3095,10 @@ async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {
const batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}. 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} PERSONNALITÉ: ${personality.nom}
DESCRIPTION: ${personality.description} DESCRIPTION: ${personality.description}
STYLE CIBLE: ${personality.style} STYLE CIBLE: ${personality.style} adapté au web professionnel
VOCABULAIRE: ${personality.vocabulairePref} VOCABULAIRE: ${personality.vocabulairePref}
CONNECTEURS: ${personality.connecteursPref} CONNECTEURS: ${personality.connecteursPref}
NIVEAU TECHNIQUE: ${personality.niveauTechnique} NIVEAU TECHNIQUE: ${personality.niveauTechnique}
@ -3149,9 +3152,8 @@ etc...`;
/** /**
* Sleep function replacement for Utilities.sleep * 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 * 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) { function createBatchFAQPairsPrompt(faqPairs, csvData) {
const personality = csvData.personality; const personality = csvData.personality;
let prompt = `PERSONNALITÉ: ${personality.nom} | ${personality.description} let prompt = `=== 1. CONTEXTE ===
STYLE: ${personality.style} Entreprise: Autocollant.fr - signalétique personnalisée
VOCABULAIRE: ${personality.vocabulairePref} Sujet: ${csvData.mc0}
CONNECTEURS: ${personality.connecteursPref} Section: FAQ pour article SEO commercial
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
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: === 3. RÈGLES GÉNÉRALES ===
- QUESTIONS: Neutres, directes, langage client naturel (8-15 mots) - Questions naturelles de clients
- RÉPONSES: Style ${personality.style}, vocabulaire ${personality.vocabulairePref} (50-80 mots) - Réponses expertes et rassurantes
- Sujets à couvrir: prix, livraison, personnalisation, installation, durabilité - Langage professionnel mais accessible
- ÉVITE répétitions excessives et expressions trop familières - Textes rédigés humainement et de façon authentique
- Style ${personality.nom} reconnaissable mais PROFESSIONNEL - Couvrir: prix, livraison, personnalisation, installation, durabilité
- PAS de messages d'excuse ("je n'ai pas l'information") - IMPÉRATIF: Respecter strictement les contraintes XML
- RÉPONDS DIRECTEMENT par questions et réponses, sans préfixe
=== 4. PAIRES FAQ À GÉNÉRER ===
PAIRES À GÉNÉRER:
`; `;
faqPairs.forEach((pair, index) => { faqPairs.forEach((pair, index) => {
const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, ''); const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
const answerTag = pair.answer.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}] prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle
Question client sur ${csvData.mc0} Réponse ${personality.style}
`; `;
}); });
@ -3533,10 +3536,25 @@ function findAssociatedTitle(textElement, existingResults) {
function createBatchBasePrompt(elements, type, csvData, existingResults = {}) { function createBatchBasePrompt(elements, type, csvData, existingResults = {}) {
const personality = csvData.personality; const personality = csvData.personality;
let prompt = `RÉDACTEUR: ${personality.nom} | Style: ${personality.style} let prompt = `=== 1. CONTEXTE ===
SUJET: ${csvData.mc0} 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 // 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) { if (generatedTitles.length > 0) {
prompt += ` prompt += `
CONTEXTE - TITRES GÉNÉRÉS: Titres existants pour contexte:
${generatedTitles.join('\n')} ${generatedTitles.join('\n')}
`; `;
@ -3560,34 +3578,33 @@ ${generatedTitles.join('\n')}
prompt += `${index + 1}. [${cleanTag}] `; prompt += `${index + 1}. [${cleanTag}] `;
// INSTRUCTIONS SPÉCIFIQUES ET COURTES PAR TYPE // INSTRUCTIONS PROPRES PAR ÉLÉMENT
if (type === 'titre') { if (type === 'titre') {
if (elementInfo.element.type === 'titre_h1') { 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') { } 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') { } 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 { } 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') { } else if (type === 'texte') {
const wordCount = elementInfo.element.name && elementInfo.element.name.includes('H2') ? '150' : '100'; prompt += `Paragraphe informatif\n`;
prompt += `Paragraphe ${wordCount} mots, style ${personality.style}\n`;
// ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT // ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT
const associatedTitle = findAssociatedTitle(elementInfo, existingResults); const associatedTitle = findAssociatedTitle(elementInfo, existingResults);
if (associatedTitle) { if (associatedTitle) {
prompt += ` Développe le titre: "${associatedTitle}"\n`; prompt += ` Contexte: "${associatedTitle}"\n`;
} }
if (elementInfo.element.resolvedContent) { if (elementInfo.element.resolvedContent) {
prompt += ` Thème: "${elementInfo.element.resolvedContent}"\n`; prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`;
} }
} else if (type === 'intro') { } else if (type === 'intro') {
prompt += `Introduction 80-100 mots, ton accueillant\n`; prompt += `Introduction engageante\n`;
} else { } else {
prompt += `Contenu pertinent pour ${csvData.mc0}\n`; prompt += `Contenu pertinent\n`;
} }
}); });
@ -3597,15 +3614,16 @@ ${generatedTitles.join('\n')}
- Phrases: ${personality.longueurPhrases} - Phrases: ${personality.longueurPhrases}
- Niveau technique: ${personality.niveauTechnique} - Niveau technique: ${personality.niveauTechnique}
CONSIGNES STRICTES: CONSIGNES STRICTES POUR ARTICLE SEO:
- RESPECTE le style ${personality.style} de ${personality.nom} mais RESTE PROFESSIONNEL - CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels
- INTERDICTION ABSOLUE: "du coup", "bon", "alors", "franchement", "nickel", "tip-top", "costaud" en excès - STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel
- VARIE les connecteurs: ${personality.connecteursPref} - INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top")
- POUR LES TITRES: SEULEMENT le titre réel, JAMAIS de référence "Titre_H1_1" ou "Titre_H2_7" - VOCABULAIRE: Mélange expertise technique + accessibilité client
- EXEMPLE TITRE: "Plaques personnalisées résistantes aux intempéries" PAS "Titre_H2_1" - SEO: Utilise naturellement "${csvData.mc0}" et termes associés
- RÉPONDS DIRECTEMENT par le contenu demandé, SANS introduction ni nom de tag - POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7"
- PAS de message d'excuse du type "je n'ai pas l'information" - EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024"
- CONTENU cohérent et professionnel, évite la sur-familiarité - 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)' : ''}: FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}:
[${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}] [${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
@ -3696,107 +3714,11 @@ function cleanXMLTagsFromContent(content) {
// ============= PARSING FUNCTIONS ============= // ============= PARSING FUNCTIONS =============
/** // FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel
* 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;
}
/** // FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées 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;
}
/** // Placeholder pour les fonctions de parsing conservées qui suivent
* 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;
}
function parseTransitionsBatchResponse(response, chunk) { function parseTransitionsBatchResponse(response, chunk) {
const results = {}; 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 ============= // ============= EXPORTS =============
module.exports = { module.exports = {
@ -4110,7 +4208,11 @@ module.exports = {
generateFAQPairsRestored, generateFAQPairsRestored,
createBatchFAQPairsPrompt, createBatchFAQPairsPrompt,
parseFAQPairsResponse, parseFAQPairsResponse,
cleanFAQInstructions cleanFAQInstructions,
extractAllTechnicalTermsBatch,
enhanceAllElementsTechnicalBatch,
parseAllTechnicalTermsResponse,
parseTechnicalEnhancementBatchResponse
}; };
/* /*
@ -5107,6 +5209,31 @@ const SHEET_CONFIG = {
keyFile: './seo-generator-470715-85d4a971c1af.json' 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 ============= // ============= TRIGGER PRINCIPAL REMPLACÉ PAR WEBHOOK/API =============
/** /**
@ -5549,22 +5676,42 @@ let logViewerLaunched = false;
* Lancer le log viewer dans Edge * Lancer le log viewer dans Edge
*/ */
function launchLogViewer() { function launchLogViewer() {
if (logViewerLaunched) return; if (logViewerLaunched || process.env.NODE_ENV === 'test') return;
try { try {
const logViewerPath = path.join(__dirname, '..', 'logs-viewer.html'); const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html');
const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`; const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`;
// Lancer Edge avec l'URL du fichier // Détecter l'environnement et adapter la commande
const edgeProcess = spawn('cmd', ['/c', 'start', 'msedge', fileUrl], { const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP;
detached: true, const isWindows = process.platform === 'win32';
stdio: 'ignore'
}); 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; logViewerLaunched = true;
logSh('🌐 Log viewer lancé', 'INFO');
logSh('🌐 Log viewer ouvert dans Edge', 'INFO');
} catch (error) { } catch (error) {
logSh(`⚠️ Impossible d'ouvrir le log viewer: ${error.message}`, 'WARNING'); logSh(`⚠️ Impossible d'ouvrir le log viewer: ${error.message}`, 'WARNING');
} }

View File

@ -43,12 +43,13 @@ const fileDest = pino.destination({
}); });
tee.pipe(fileDest); tee.pipe(fileDest);
// Custom levels for Pino to include TRACE and PROMPT // Custom levels for Pino to include TRACE, PROMPT, and LLM
const customLevels = { const customLevels = {
trace: 5, // Below debug (10) trace: 5, // Below debug (10)
debug: 10, debug: 10,
info: 20, info: 20,
prompt: 25, // New level for prompts (between info and warn) prompt: 25, // New level for prompts (between info and warn)
llm: 26, // New level for LLM interactions (between prompt and warn)
warn: 30, warn: 30,
error: 40, error: 40,
fatal: 50 fatal: 50
@ -164,6 +165,9 @@ async function logSh(message, level = 'INFO') {
case 'prompt': case 'prompt':
logger.prompt(traceData, message); logger.prompt(traceData, message);
break; break;
case 'llm':
logger.llm(traceData, message);
break;
default: default:
logger.info(traceData, message); logger.info(traceData, message);
} }

View File

@ -125,7 +125,10 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA // 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT'); logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
logSh(prompt, '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 // Préparer la requête selon le provider
const requestData = buildRequestData(llmProvider, prompt, options, personality); 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 // Parser la réponse selon le format du provider
const content = parseResponse(llmProvider, response); 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; 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 // Enregistrer les stats d'usage
await recordUsageStats(llmProvider, prompt.length, content.length, duration); await recordUsageStats(llmProvider, prompt.length, content.length, duration);

View File

@ -30,7 +30,7 @@ function launchLogViewer() {
if (logViewerLaunched || process.env.NODE_ENV === 'test') return; if (logViewerLaunched || process.env.NODE_ENV === 'test') return;
try { try {
const logViewerPath = path.join(__dirname, '..', 'logs-viewer.html'); const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html');
const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`; const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`;
// Détecter l'environnement et adapter la commande // Détecter l'environnement et adapter la commande

View File

@ -8,6 +8,11 @@ const { logSh } = require('./ErrorReporting');
const { tracer } = require('./trace.js'); const { tracer } = require('./trace.js');
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig'); 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 * NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection * 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 * ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
* OPTIMISATION : 1 appel extraction + 1 appel enhancement au lieu de 20+ * NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
*/ */
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) { async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO'); 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) { async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
const contentEntries = Object.keys(baseContents); const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
CONTENUS À ANALYSER: CONTENU À ANALYSER:
TAG: ${tag}
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag} CONTENU: "${content}"
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
CONSIGNES: 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.) - Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
- Focus: matériaux, procédés, normes, dimensions, technologies - Focus: matériaux, procédés, normes, dimensions, technologies spécifiques
- Si aucun terme technique "AUCUN"
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance
FORMAT RÉPONSE EXACT: RÉPONSE REQUISE:
[1] dibond, impression UV, 3mm OU AUCUN - Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
[2] aluminium, fraisage CNC OU AUCUN - Si aucun terme technique: "NON"
[3] AUCUN
etc... (${contentEntries.length} lignes total)`; EXEMPLE:
OUI - termes: aluminium composite, impression numérique, gravure laser`;
try { try {
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, { const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
temperature: 0.3,
maxTokens: 2000
}, csvData.personality);
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
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) { } catch (error) {
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR'); logSh(`ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`); 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) { async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
if (elementsNeedingEnhancement.length === 0) return {}; const prompt = `MISSION: Améliore ce contenu en intégrant des termes techniques précis.
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 CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
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: CONTENU À AMÉLIORER:
TAG: ${tag}
CONTENU: "${content}"
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag} OBJECTIFS:
CONTENU ACTUEL: "${item.content}" - Remplace les termes génériques par des termes techniques précis
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')} - 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: EXEMPLE DE TRANSFORMATION:
- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom} "matériaux haute performance" "dibond 3mm ou aluminium composite"
- GARDE la même longueur, structure et ton "impression moderne" "impression UV haute définition"
- Intègre naturellement les termes techniques listés "fixation solide" "fixation par chevilles inox Ø6mm"
- 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: CONTRAINTES:
[1] Contenu avec amélioration technique selon ${csvData.personality?.nom} - GARDE la même structure
[2] Contenu avec amélioration technique selon ${csvData.personality?.nom} - MÊME longueur approximative
etc... (${elementsNeedingEnhancement.length} éléments total)`; - Style cohérent avec l'original
- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;
try { try {
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, { const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
temperature: 0.4, return enhancedContent.trim();
maxTokens: 5000 // Plus large pour batch total
}, csvData.personality);
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
} catch (error) { } catch (error) {
logSh(`FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR'); logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`); 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 * ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable
*/ */
@ -564,9 +568,8 @@ etc...`;
/** /**
* Sleep function replacement for Utilities.sleep * 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 * RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
@ -1127,107 +1130,11 @@ function cleanXMLTagsFromContent(content) {
// ============= PARSING FUNCTIONS ============= // ============= PARSING FUNCTIONS =============
/** // FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel
* 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;
}
/** // FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées 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;
}
/** // Placeholder pour les fonctions de parsing conservées qui suivent
* 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;
}
function parseTransitionsBatchResponse(response, chunk) { function parseTransitionsBatchResponse(response, chunk) {
const results = {}; 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 ============= // ============= EXPORTS =============
module.exports = { module.exports = {
@ -1541,5 +1624,9 @@ module.exports = {
generateFAQPairsRestored, generateFAQPairsRestored,
createBatchFAQPairsPrompt, createBatchFAQPairsPrompt,
parseFAQPairsResponse, parseFAQPairsResponse,
cleanFAQInstructions cleanFAQInstructions,
extractAllTechnicalTermsBatch,
enhanceAllElementsTechnicalBatch,
parseAllTechnicalTermsResponse,
parseTechnicalEnhancementBatchResponse
}; };

179
tools/log-server.cjs Normal file
View File

@ -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(`
<!DOCTYPE html>
<html>
<head>
<title>Log Viewer Server</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
h1 { color: #333; }
.log-list { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.log-item {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.log-item:hover { background: #f8f9fa; }
.log-name { font-weight: bold; color: #2c5aa0; }
.log-info { font-size: 0.9em; color: #666; }
.view-btn {
background: #007bff;
color: white;
padding: 5px 15px;
text-decoration: none;
border-radius: 3px;
font-size: 0.9em;
}
.view-btn:hover { background: #0056b3; }
.realtime-btn {
background: #28a745;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
display: inline-block;
margin-bottom: 20px;
}
.realtime-btn:hover { background: #218838; }
</style>
</head>
<body>
<h1>📊 SEO Generator - Log Viewer</h1>
<a href="/tools/logs-viewer.html" class="realtime-btn">🔴 Logs en temps réel</a>
<div class="log-list">
<h2>Fichiers de log disponibles</h2>
<div id="logFiles">Chargement...</div>
</div>
<script>
async function loadLogFiles() {
try {
const response = await fetch('/api/logs');
const data = await response.json();
const container = document.getElementById('logFiles');
if (data.files.length === 0) {
container.innerHTML = '<p>Aucun fichier de log trouvé</p>';
return;
}
container.innerHTML = data.files.map(file => {
const sizeKB = Math.round(file.size / 1024);
const date = new Date(file.modified).toLocaleString('fr-FR');
return \`
<div class="log-item">
<div>
<div class="log-name">\${file.name}</div>
<div class="log-info">\${sizeKB} KB \${date}</div>
</div>
<a href="\${file.url}" class="view-btn" target="_blank">Voir</a>
</div>
\`;
}).join('');
} catch (error) {
document.getElementById('logFiles').innerHTML =
'<p style="color: red;">Erreur: ' + error.message + '</p>';
}
}
loadLogFiles();
</script>
</body>
</html>
`);
});
// 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);
});

921
tools/logs-viewer.html Normal file
View File

@ -0,0 +1,921 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SEO Generator - Logs en temps réel</title>
<style>
body {
font-family: 'Courier New', monospace;
background: #1e1e1e;
color: #ffffff;
margin: 0;
padding: 4px;
}
.header {
background: #2d2d30;
padding: 4px;
border-radius: 2px;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
margin: 0;
font-size: 12px;
}
.header-right {
display: flex;
gap: 4px;
align-items: center;
}
.status {
display: inline-block;
padding: 2px 4px;
border-radius: 1px;
font-size: 9px;
font-weight: bold;
}
.status.connected { background: #28a745; }
.status.disconnected { background: #dc3545; }
.status.connecting { background: #ffc107; color: #000; }
.logs-container {
height: calc(100vh - 88px);
overflow-y: auto;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 2px;
padding: 4px;
}
.log-entry {
padding: 2px 0;
border-bottom: 1px solid #21262d;
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.log-entry.unwrapped {
white-space: pre-wrap;
overflow: visible;
text-overflow: unset;
background: rgba(88, 166, 255, 0.05);
border-left: 2px solid #58a6ff;
padding-left: 4px;
}
.log-entry:last-child {
border-bottom: none;
}
.log-entry.trace {
background: rgba(31, 111, 235, 0.1);
padding-left: 1px;
border-left: 2px solid #1f6feb;
}
.log-entry.trace.span-start {
border-left-color: #28a745;
}
.log-entry.trace.span-end {
border-left-color: #17a2b8;
}
.log-entry.trace.span-error {
border-left-color: #dc3545;
background: rgba(220, 53, 69, 0.1);
}
.log-entry.stack-trace {
background: rgba(220, 53, 69, 0.05);
padding-left: 1px;
color: #f85149;
font-family: 'Courier New', monospace;
font-size: 10px;
border-left: 2px solid #dc3545;
}
.log-details {
margin-top: 4px;
padding: 4px;
background: rgba(139, 148, 158, 0.1);
border-radius: 2px;
font-size: 9px;
color: #8b949e;
display: none;
}
.show-details .log-details {
display: block;
}
.details-toggle {
background: none;
color: #58a6ff;
border: 1px solid #58a6ff;
padding: 1px 1px;
font-size: 8px;
margin-right: 4px;
}
.details-toggle:hover {
background: rgba(88, 166, 255, 0.1);
}
.unwrap-toggle {
background: none;
color: #f79009;
border: 1px solid #f79009;
padding: 1px 1px;
font-size: 8px;
margin-right: 4px;
}
.unwrap-toggle:hover {
background: rgba(247, 144, 9, 0.1);
}
.search-container {
margin-bottom: 3px;
display: flex;
gap: 4px;
align-items: center;
}
.search-input {
flex-grow: 1;
background: #21262d;
border: 1px solid #30363d;
color: #f0f6fc;
padding: 4px 6px;
border-radius: 2px;
font-size: 11px;
}
.search-input:focus {
outline: none;
border-color: #58a6ff;
background: #0d1117;
}
.search-info {
color: #7d8590;
font-size: 10px;
min-width: 80px;
}
.log-entry.search-match {
background: rgba(255, 193, 7, 0.2);
border-left: 3px solid #ffc107;
}
.log-entry.search-current {
background: rgba(255, 193, 7, 0.4);
border-left: 3px solid #ffc107;
}
.search-highlight {
background: #ffc107;
color: #000;
padding: 1px 2px;
border-radius: 2px;
}
.timestamp {
color: #7d8590;
margin-right: 1px;
font-size: 11px;
}
.level {
font-weight: bold;
margin-right: 1px;
padding: 1px 1px;
border-radius: 2px;
font-size: 11px;
min-width: 32px;
}
.level.INFO { background: #1f6feb; }
.level.WARN, .level.WARNING { background: #d29922; }
.level.ERROR { background: #da3633; }
.level.DEBUG { background: #8b949e; }
.level.TRACE { background: #238636; }
.level.PROMPT { background: #8b5cf6; }
.level.LLM { background: #f97316; }
button {
background: #238636;
color: white;
border: none;
padding: 3px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
button:hover { background: #2ea043; }
button:disabled { background: #6e7781; cursor: not-allowed; }
.filter-toggles {
display: flex;
gap: 2px;
align-items: center;
margin-left: 6px;
}
.filter-toggle {
background: #21262d;
border: 1px solid #30363d;
color: #f0f6fc;
padding: 2px 4px;
border-radius: 1px;
cursor: pointer;
font-size: 9px;
min-width: 40px;
text-align: center;
}
.filter-toggle.active.trace { background: #238636; border-color: #238636; }
.filter-toggle.active.info { background: #1f6feb; border-color: #1f6feb; }
.filter-toggle.active.debug { background: #8b949e; border-color: #8b949e; }
.filter-toggle.active.warn { background: #d29922; border-color: #d29922; }
.filter-toggle.active.error { background: #da3633; border-color: #da3633; }
.filter-toggle.active.prompt { background: #8b5cf6; border-color: #8b5cf6; }
.filter-toggle:hover { background: #30363d; }
.log-entry.hidden-by-filter { display: none !important; }
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<h1>SEO Generator - Logs temps réel</h1>
<span id="status" class="status connecting">Connexion...</span>
<span style="margin-left: 15px; font-size: 12px;">Port: <strong>8081</strong></span>
<br>
<button onclick="toggleGlobalDetails()" id="detailsBtn">Mode détaillé: OFF</button>
<button onclick="toggleLineUnwrap()" id="lineUnwrapBtn">Unwrap ligne: OFF</button>
</div>
<div class="header-right">
<div class="filter-toggles">
<span style="color: #7d8590; font-size: 11px;">Filtres:</span>
<button class="filter-toggle active trace" onclick="toggleLevelFilter('trace')" id="traceFilter">TRACE</button>
<button class="filter-toggle active info" onclick="toggleLevelFilter('info')" id="infoFilter">INFO</button>
<button class="filter-toggle active debug" onclick="toggleLevelFilter('debug')" id="debugFilter">DEBUG</button>
<button class="filter-toggle active warn" onclick="toggleLevelFilter('warn')" id="warnFilter">WARN</button>
<button class="filter-toggle active error" onclick="toggleLevelFilter('error')" id="errorFilter">ERROR</button>
<button class="filter-toggle active prompt" onclick="toggleLevelFilter('prompt')" id="promptFilter">PROMPT</button>
<button class="filter-toggle active llm" onclick="toggleLevelFilter('llm')" id="llmFilter">LLM</button>
</div>
<button onclick="clearLogs()">Effacer</button>
<button onclick="toggleAutoScroll()" id="autoScrollBtn">Auto-scroll: ON</button>
<button onclick="reconnect()" id="reconnectBtn">Reconnecter</button>
</div>
</div>
<div class="search-container">
<input type="text" class="search-input" id="searchInput" placeholder="Rechercher dans les logs... (Ctrl+F)">
<div class="search-info" id="searchInfo">0 résultats</div>
<button onclick="searchPrevious()" id="searchPrevBtn" disabled>⬆ Précédent</button>
<button onclick="searchNext()" id="searchNextBtn" disabled>⬇ Suivant</button>
<button onclick="clearSearch()" id="clearSearchBtn"></button>
</div>
<div class="logs-container" id="logsContainer">
<div class="log-entry">
<span class="timestamp">--:--:--</span>
<span class="level INFO">INFO</span>
En attente des logs...
</div>
</div>
<script>
let ws;
let autoScroll = true;
const logsContainer = document.getElementById('logsContainer');
const statusElement = document.getElementById('status');
// Variables de recherche
let searchMatches = [];
let currentMatchIndex = -1;
let searchTerm = '';
// Variables de filtrage
let levelFilters = {
trace: true,
info: true,
debug: true,
warn: true,
warning: true,
error: true,
prompt: true,
llm: true
};
// Récupérer le fichier de log depuis l'URL
const urlParams = new URLSearchParams(window.location.search);
const logFile = urlParams.get('file');
console.log('🌐 URL params:', window.location.search, 'logFile:', logFile);
if (logFile) {
// Mode fichier : charger le fichier spécifié
console.log('📁 MODE FICHIER activé pour:', logFile);
document.title = `SEO Generator - Logs: ${logFile}`;
document.querySelector('h1').textContent = `Logs: ${logFile}`;
loadLogFile(logFile);
} else {
// Mode temps réel : WebSocket comme avant
console.log('⚡ MODE WEBSOCKET activé - pas de paramètre file');
connect();
}
async function loadLogFile(filename) {
try {
statusElement.textContent = `Chargement ${filename}...`;
statusElement.className = 'status connecting';
// Utiliser file:// pour lire directement le fichier local
const input = document.createElement('input');
input.type = 'file';
input.accept = '.log';
input.style.display = 'none';
input.onchange = function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const logContent = e.target.result;
const lines = logContent.split('\n').filter(line => line.trim());
statusElement.textContent = `Fichier chargé (${lines.length} lignes)`;
statusElement.className = 'status connected';
// Parser et afficher chaque ligne
lines.forEach(line => {
try {
const logData = JSON.parse(line);
const timestamp = new Date(logData.time).toISOString();
const level = normalizeLevelName(logData.level);
addLogEntry(logData.msg || logData.message || line, level, timestamp, line);
} catch (error) {
// Ligne non-JSON, afficher telle quelle
addLogEntry(line, 'INFO', new Date().toISOString(), line);
}
});
};
reader.readAsText(file);
};
// Si un nom de fichier est spécifié, tenter de le charger depuis logs/
if (filename) {
try {
const response = await fetch(`logs/${filename}`);
if (response.ok) {
const logContent = await response.text();
const lines = logContent.split('\n').filter(line => line.trim());
statusElement.textContent = `Fichier chargé (${lines.length} lignes)`;
statusElement.className = 'status connected';
lines.forEach(line => {
try {
const logData = JSON.parse(line);
const timestamp = new Date(logData.time).toISOString();
const level = normalizeLevelName(logData.level);
addLogEntry(logData.msg || logData.message || line, level, timestamp, line);
} catch (error) {
addLogEntry(line, 'INFO', new Date().toISOString(), line);
}
});
return;
}
} catch (fetchError) {
// Si le fetch échoue, demander à l'utilisateur de sélectionner le fichier
}
}
// Demander à l'utilisateur de sélectionner le fichier
addLogEntry(`Sélectionnez le fichier de log ${filename || ''} à charger`, 'INFO');
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
} catch (error) {
statusElement.textContent = `Erreur: ${error.message}`;
statusElement.className = 'status disconnected';
addLogEntry(`Erreur chargement fichier: ${error.message}`, 'ERROR');
}
}
function normalizeLevelName(level) {
const levelMap = {10:'TRACE',20:'DEBUG',25:'PROMPT',26:'LLM',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'};
if (typeof level === 'number') {
return levelMap[level] || 'INFO';
}
return String(level).toUpperCase();
}
function connect() {
console.log('🔌 connect() appelé - tentative WebSocket ws://localhost:8081');
ws = new WebSocket('ws://localhost:8081');
ws.onopen = () => {
console.log('✅ WebSocket connecté !');
statusElement.textContent = 'Connecté';
statusElement.className = 'status connected';
// Reset des tentatives de reconnexion
reconnectAttempts = 0;
reconnectDelay = 1000; // Reconnexion ultra rapide
};
ws.onmessage = (event) => {
console.log('📨 Message WebSocket reçu:', event.data);
try {
const logData = JSON.parse(event.data);
addLogEntry(logData.message, logData.level, logData.timestamp, event.data);
} catch (error) {
console.log('❌ Erreur parsing:', error);
addLogEntry('Erreur parsing log: ' + event.data, 'ERROR');
}
};
ws.onclose = () => {
statusElement.textContent = 'Déconnecté';
statusElement.className = 'status disconnected';
// Auto-reconnexion immédiate
scheduleReconnect();
};
ws.onerror = (error) => {
statusElement.textContent = 'Erreur';
statusElement.className = 'status disconnected';
// Auto-reconnexion immédiate
scheduleReconnect();
};
}
let showDetailsMode = false;
function addLogEntry(message, level = 'INFO', timestamp = null, rawData = null) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const time = timestamp ? new Date(timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
// Déterminer si c'est une trace et son type
let traceClass = '';
let cleanMessage = message;
if (message.includes('▶')) {
traceClass = 'trace span-start';
// Nettoyer le message pour garder uniquement l'info utile
cleanMessage = message.replace('▶ ', '🔵 ');
} else if (message.includes('✔')) {
traceClass = 'trace span-end';
cleanMessage = message.replace('✔ ', '✅ ');
} else if (message.includes('✖')) {
traceClass = 'trace span-error';
cleanMessage = message.replace('✖ ', '❌ ');
} else if (message.includes('•')) {
traceClass = 'trace';
cleanMessage = message.replace('• ', '📝 ');
} else if (message.includes('Stack trace:') || message.trim().startsWith('at ')) {
traceClass = 'stack-trace';
if (message.includes('Stack trace:')) {
cleanMessage = '🔴 ' + message;
} else {
cleanMessage = ' ' + message; // Indentation pour les lignes de stack
}
}
logEntry.className += ' ' + traceClass;
const hasDetails = rawData && rawData !== JSON.stringify({message, level, timestamp});
const detailsButton = hasDetails ?
`<button class="details-toggle" onclick="toggleDetails(this)">détails</button>` :
`<span style="display: inline-block; width: 41px;"></span>`; // Placeholder pour alignement
// Détecter si le message est trop long (approximation simple)
const isMessageTooLong = cleanMessage.length > 80;
const unwrapButton = isMessageTooLong ?
`<button class="unwrap-toggle" onclick="toggleUnwrap(this)">unwrap</button>` :
`<span style="display: inline-block; width: 41px;"></span>`; // Placeholder pour alignement
logEntry.innerHTML = `
${detailsButton}
${unwrapButton}
<span class="timestamp">${time}</span>
<span class="level ${level}">${level}</span>
${cleanMessage}
${hasDetails ? `<div class="log-details"><pre>${JSON.stringify(JSON.parse(rawData), null, 2)}</pre></div>` : ''}
`;
// Appliquer le mode détails global si activé
if (showDetailsMode && hasDetails) {
logEntry.classList.add('show-details');
}
// Appliquer les filtres de niveau
applyLevelFilterToEntry(logEntry, level);
// Ajouter le click listener pour l'unwrap ligne par ligne
logEntry.addEventListener('click', (e) => {
// Ne pas déclencher si on clique sur un bouton
if (e.target.classList.contains('details-toggle') ||
e.target.classList.contains('unwrap-toggle')) return;
toggleLogEntryWrap(logEntry);
});
logsContainer.appendChild(logEntry);
// Auto-scroll intelligent : seulement si l'utilisateur est déjà en bas
if (autoScroll) {
// Détection plus précise : considérer qu'on est "en bas" si on est à moins de 100px du bas
const scrollTop = logsContainer.scrollTop;
const scrollHeight = logsContainer.scrollHeight;
const clientHeight = logsContainer.clientHeight;
const isAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 100);
if (isAtBottom) {
// Scroll immédiat vers le bas
requestAnimationFrame(() => {
logsContainer.scrollTop = logsContainer.scrollHeight;
});
}
}
}
function toggleDetails(button) {
const logEntry = button.parentElement;
logEntry.classList.toggle('show-details');
button.textContent = logEntry.classList.contains('show-details') ? 'masquer' : 'détails';
}
function toggleUnwrap(button) {
const logEntry = button.parentElement;
if (logEntry.classList.contains('unwrapped')) {
// Remettre en mode wrapped
logEntry.classList.remove('unwrapped');
logEntry.style.whiteSpace = 'nowrap';
logEntry.style.overflow = 'hidden';
logEntry.style.textOverflow = 'ellipsis';
button.textContent = 'unwrap';
} else {
// Passer en mode unwrapped
logEntry.classList.add('unwrapped');
logEntry.style.whiteSpace = 'pre-wrap';
logEntry.style.overflow = 'visible';
logEntry.style.textOverflow = 'unset';
button.textContent = 'wrap';
}
}
function toggleGlobalDetails() {
showDetailsMode = !showDetailsMode;
const detailsBtn = document.getElementById('detailsBtn');
detailsBtn.textContent = `Mode détaillé: ${showDetailsMode ? 'ON' : 'OFF'}`;
// Appliquer/retirer le mode détails à toutes les entrées
const entries = document.querySelectorAll('.log-entry');
entries.forEach(entry => {
if (showDetailsMode) {
entry.classList.add('show-details');
const toggle = entry.querySelector('.details-toggle');
if (toggle) toggle.textContent = 'masquer';
} else {
entry.classList.remove('show-details');
const toggle = entry.querySelector('.details-toggle');
if (toggle) toggle.textContent = 'détails';
}
});
}
function clearLogs() {
logsContainer.innerHTML = '';
addLogEntry('Logs effacés', 'INFO');
}
function toggleAutoScroll() {
autoScroll = !autoScroll;
document.getElementById('autoScrollBtn').textContent = `Auto-scroll: ${autoScroll ? 'ON' : 'OFF'}`;
}
// Variables pour le unwrap ligne par ligne
let lineUnwrapMode = false;
function toggleLineUnwrap() {
lineUnwrapMode = !lineUnwrapMode;
document.getElementById('lineUnwrapBtn').textContent = `Unwrap ligne: ${lineUnwrapMode ? 'ON' : 'OFF'}`;
if (!lineUnwrapMode) {
// Désactiver le mode : remettre toutes les lignes en mode compact
const logEntries = document.querySelectorAll('.log-entry');
logEntries.forEach(entry => {
entry.classList.remove('unwrapped');
});
}
}
// Fonction pour unwrap/wrap une ligne individuelle
function toggleLogEntryWrap(logEntry) {
if (!lineUnwrapMode) return; // Mode désactivé
if (logEntry.classList.contains('unwrapped')) {
// Re-wrapper la ligne
logEntry.classList.remove('unwrapped');
} else {
// Unwrapper la ligne
logEntry.classList.add('unwrapped');
}
}
function reconnect() {
if (ws) {
ws.close();
}
statusElement.textContent = 'Reconnexion...';
statusElement.className = 'status connecting';
setTimeout(connect, 1000);
}
// Fonctions de recherche
function performSearch() {
const searchInput = document.getElementById('searchInput');
const searchInfo = document.getElementById('searchInfo');
const searchPrevBtn = document.getElementById('searchPrevBtn');
const searchNextBtn = document.getElementById('searchNextBtn');
searchTerm = searchInput.value.trim().toLowerCase();
// Effacer les recherches précédentes
clearSearchHighlights();
searchMatches = [];
currentMatchIndex = -1;
if (searchTerm === '') {
searchInfo.textContent = '0 résultats';
searchPrevBtn.disabled = true;
searchNextBtn.disabled = true;
return;
}
// Rechercher dans tous les logs visibles
const logEntries = document.querySelectorAll('.log-entry:not(.hidden-by-filter)');
logEntries.forEach((entry, index) => {
const text = entry.textContent.toLowerCase();
if (text.includes(searchTerm)) {
searchMatches.push(entry);
entry.classList.add('search-match');
// Highlighter le texte
highlightTextInElement(entry, searchTerm);
}
});
// Mettre à jour l'interface
searchInfo.textContent = `${searchMatches.length} résultat${searchMatches.length > 1 ? 's' : ''}`;
searchPrevBtn.disabled = searchMatches.length === 0;
searchNextBtn.disabled = searchMatches.length === 0;
// Aller au premier résultat
if (searchMatches.length > 0) {
currentMatchIndex = 0;
scrollToCurrentMatch();
}
}
function highlightTextInElement(element, term) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
if (node.textContent.toLowerCase().includes(term)) {
textNodes.push(node);
}
}
textNodes.forEach(textNode => {
const parent = textNode.parentNode;
const text = textNode.textContent;
const lowerText = text.toLowerCase();
const regex = new RegExp(`(${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
if (lowerText.includes(term)) {
const highlightedHTML = text.replace(regex, '<span class="search-highlight">$1</span>');
const wrapper = document.createElement('span');
wrapper.innerHTML = highlightedHTML;
parent.insertBefore(wrapper, textNode);
parent.removeChild(textNode);
}
});
}
function clearSearchHighlights() {
const highlights = document.querySelectorAll('.search-highlight');
highlights.forEach(highlight => {
const parent = highlight.parentNode;
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
parent.normalize();
});
const searchMatches = document.querySelectorAll('.search-match, .search-current');
searchMatches.forEach(match => {
match.classList.remove('search-match', 'search-current');
});
}
function scrollToCurrentMatch() {
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
// Retirer la classe current de l'ancien match
searchMatches.forEach(match => match.classList.remove('search-current'));
// Ajouter la classe current au match actuel
const currentMatch = searchMatches[currentMatchIndex];
currentMatch.classList.add('search-current');
// Scroller vers l'élément
currentMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Mettre à jour l'info de recherche
document.getElementById('searchInfo').textContent =
`${currentMatchIndex + 1}/${searchMatches.length} résultat${searchMatches.length > 1 ? 's' : ''}`;
}
}
function searchNext() {
if (searchMatches.length > 0) {
currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
scrollToCurrentMatch();
}
}
function searchPrevious() {
if (searchMatches.length > 0) {
currentMatchIndex = currentMatchIndex === 0 ? searchMatches.length - 1 : currentMatchIndex - 1;
scrollToCurrentMatch();
}
}
function clearSearch() {
document.getElementById('searchInput').value = '';
clearSearchHighlights();
searchMatches = [];
currentMatchIndex = -1;
document.getElementById('searchInfo').textContent = '0 résultats';
document.getElementById('searchPrevBtn').disabled = true;
document.getElementById('searchNextBtn').disabled = true;
}
// Event listeners pour la recherche
document.getElementById('searchInput').addEventListener('input', performSearch);
document.getElementById('searchInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if (e.shiftKey) {
searchPrevious();
} else {
searchNext();
}
} else if (e.key === 'Escape') {
clearSearch();
}
});
// Fonctions de filtrage par niveau
function applyLevelFilterToEntry(entry, level) {
const normalizedLevel = level.toLowerCase();
if (!levelFilters[normalizedLevel]) {
entry.classList.add('hidden-by-filter');
} else {
entry.classList.remove('hidden-by-filter');
}
}
function toggleLevelFilter(level) {
levelFilters[level] = !levelFilters[level];
levelFilters['warning'] = levelFilters['warn']; // Synchroniser warn/warning
const button = document.getElementById(`${level}Filter`);
if (levelFilters[level]) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
// Capturer le pourcentage de position AVANT d'appliquer le filtre
const currentScroll = logsContainer.scrollTop;
const maxScroll = logsContainer.scrollHeight - logsContainer.clientHeight;
const currentViewPercentage = maxScroll > 0 ? currentScroll / maxScroll : 0;
// Appliquer les filtres à tous les logs
const entries = document.querySelectorAll('.log-entry');
entries.forEach(entry => {
const entryLevel = entry.querySelector('.level').textContent.toLowerCase();
applyLevelFilterToEntry(entry, entryLevel);
});
// Re-effectuer la recherche si active
if (searchTerm) {
performSearch();
}
// Scroll intelligent avec le pourcentage capturé
smartScrollAfterFilter(currentViewPercentage);
}
function smartScrollAfterFilter(currentViewPercentage) {
setTimeout(() => {
const visibleEntries = document.querySelectorAll('.log-entry:not(.hidden-by-filter)');
if (visibleEntries.length === 0) return;
// Si on a un match de recherche actuel, privilégier celui-ci
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
const currentSearchMatch = searchMatches[currentMatchIndex];
if (!currentSearchMatch.classList.contains('hidden-by-filter')) {
currentSearchMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
}
// Appliquer le même pourcentage aux nouvelles entrées visibles
// Attendre que le DOM se mette à jour après l'application des filtres
setTimeout(() => {
const newMaxScroll = logsContainer.scrollHeight - logsContainer.clientHeight;
const targetScroll = newMaxScroll * currentViewPercentage;
logsContainer.scrollTo({
top: Math.max(0, Math.min(targetScroll, newMaxScroll)),
behavior: 'smooth'
});
}, 50);
}, 100);
}
// Raccourci Ctrl+F
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
document.getElementById('searchInput').focus();
}
});
// Connexion initiale SEULEMENT si pas en mode fichier
// (connect() est déjà appelé dans la logique if/else plus haut)
// Auto-reconnexion intelligente
let reconnectDelay = 1000; // 1 seconde
let reconnectAttempts = 0;
let maxReconnectAttempts = 50; // Limite raisonnable
function scheduleReconnect() {
if (reconnectAttempts >= maxReconnectAttempts) {
addLogEntry('Nombre max de tentatives de reconnexion atteint', 'ERROR');
return;
}
setTimeout(() => {
if (!ws || ws.readyState === WebSocket.CLOSED) {
reconnectAttempts++;
statusElement.textContent = `Reconnexion... (${reconnectAttempts}/${maxReconnectAttempts})`;
statusElement.className = 'status connecting';
connect();
}
}, reconnectDelay);
}
// Gestion intelligente de l'auto-scroll basée sur le comportement utilisateur
let userScrolledAway = false;
let scrollTimeout;
logsContainer.addEventListener('scroll', () => {
if (!autoScroll) return;
clearTimeout(scrollTimeout);
const scrollTop = logsContainer.scrollTop;
const scrollHeight = logsContainer.scrollHeight;
const clientHeight = logsContainer.clientHeight;
const isAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 100);
if (isAtBottom) {
// L'utilisateur est revenu en bas, réactiver l'auto-scroll
if (userScrolledAway) {
userScrolledAway = false;
console.log('🔄 Auto-scroll réactivé - utilisateur revenu en bas');
}
} else {
// L'utilisateur a scrollé vers le haut, marquer qu'il s'est éloigné du bas
userScrolledAway = true;
}
// Debounce pour éviter trop d'événements
scrollTimeout = setTimeout(() => {
// Logique supplémentaire si nécessaire
}, 150);
});
// Améliorer addLogEntry pour respecter userScrolledAway
const originalAddLogEntry = addLogEntry;
function enhancedAddLogEntry(message, level = 'INFO', timestamp = null, rawData = null) {
originalAddLogEntry(message, level, timestamp, rawData);
// Override : si l'utilisateur n'a pas scrollé manuellement ET que l'auto-scroll est ON,
// forcer le scroll vers le bas
if (autoScroll && !userScrolledAway) {
requestAnimationFrame(() => {
logsContainer.scrollTop = logsContainer.scrollHeight;
});
}
}
// Remplacer la fonction globale
addLogEntry = enhancedAddLogEntry;
</script>
</body>
</html>

View File

@ -26,7 +26,7 @@ function setLogFile(filePath) { LOG_FILE = path.resolve(process.cwd(), filePath)
function MB(n){return n*1024*1024;} function MB(n){return n*1024*1024;}
function toInt(v,d){const n=parseInt(v,10);return Number.isFinite(n)?n:d;} 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){ function normLevel(v){
if (v==null) return 'UNKNOWN'; if (v==null) return 'UNKNOWN';
if (typeof v==='number') return LEVEL_MAP_NUM[v]||String(v); if (typeof v==='number') return LEVEL_MAP_NUM[v]||String(v);