Add logviewer.html, prompt improvment, clean duplication
This commit is contained in:
parent
31ea27153d
commit
ad9e3e1374
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
641
code.js
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
179
tools/log-server.cjs
Normal 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
921
tools/logs-viewer.html
Normal 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>
|
||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user