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
|
||||
test-*.js
|
||||
|
||||
# HTML généré (logs viewer)
|
||||
logs-viewer.html
|
||||
# HTML généré était ici mais maintenant on le garde dans tools/
|
||||
|
||||
# Unit test reports
|
||||
TEST*.xml
|
||||
|
||||
641
code.js
641
code.js
@ -1,6 +1,6 @@
|
||||
/*
|
||||
code.js — bundle concaténé
|
||||
Généré: 2025-09-03T04:21:57.159Z
|
||||
Généré: 2025-09-04T01:10:08.540Z
|
||||
Source: lib
|
||||
Fichiers: 16
|
||||
Ordre: topo
|
||||
@ -57,12 +57,13 @@ const fileDest = pino.destination({
|
||||
});
|
||||
tee.pipe(fileDest);
|
||||
|
||||
// Custom levels for Pino to include TRACE and PROMPT
|
||||
// Custom levels for Pino to include TRACE, PROMPT, and LLM
|
||||
const customLevels = {
|
||||
trace: 5, // Below debug (10)
|
||||
debug: 10,
|
||||
info: 20,
|
||||
prompt: 25, // New level for prompts (between info and warn)
|
||||
llm: 26, // New level for LLM interactions (between prompt and warn)
|
||||
warn: 30,
|
||||
error: 40,
|
||||
fatal: 50
|
||||
@ -178,6 +179,9 @@ async function logSh(message, level = 'INFO') {
|
||||
case 'prompt':
|
||||
logger.prompt(traceData, message);
|
||||
break;
|
||||
case 'llm':
|
||||
logger.llm(traceData, message);
|
||||
break;
|
||||
default:
|
||||
logger.info(traceData, message);
|
||||
}
|
||||
@ -1282,7 +1286,10 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
|
||||
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
|
||||
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
|
||||
logSh(prompt, 'PROMPT');
|
||||
logSh(`===== FIN PROMPT ${llmProvider.toUpperCase()} (${personality?.nom || 'AUCUNE'}) =====\n`, 'PROMPT');
|
||||
|
||||
// 📤 LOG LLM REQUEST COMPLET
|
||||
logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM');
|
||||
logSh(prompt, 'LLM');
|
||||
|
||||
// Préparer la requête selon le provider
|
||||
const requestData = buildRequestData(llmProvider, prompt, options, personality);
|
||||
@ -1293,8 +1300,12 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
|
||||
// Parser la réponse selon le format du provider
|
||||
const content = parseResponse(llmProvider, response);
|
||||
|
||||
// 📥 LOG LLM RESPONSE COMPLET
|
||||
logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM');
|
||||
logSh(content, 'LLM');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms: "${content.substring(0, 150)}${content.length > 150 ? '...' : ''}"`, 'INFO');
|
||||
logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');
|
||||
|
||||
// Enregistrer les stats d'usage
|
||||
await recordUsageStats(llmProvider, prompt.length, content.length, duration);
|
||||
@ -1727,6 +1738,8 @@ module.exports = {
|
||||
LLM_CONFIG
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ File: lib/ElementExtraction.js │
|
||||
@ -2309,38 +2322,21 @@ CONTEXTE:
|
||||
`;
|
||||
|
||||
missingElements.forEach((missing, index) => {
|
||||
prompt += `${index + 1}. [${missing.name}] `;
|
||||
|
||||
// INSTRUCTIONS SPÉCIFIQUES PAR TYPE
|
||||
if (missing.type.includes('titre_h1')) {
|
||||
prompt += `→ Titre H1 principal (8-10 mots) pour ${contextAnalysis.mainKeyword}\n`;
|
||||
} else if (missing.type.includes('titre_h2')) {
|
||||
prompt += `→ Titre H2 section (6-8 mots) lié à ${contextAnalysis.mainKeyword}\n`;
|
||||
} else if (missing.type.includes('titre_h3')) {
|
||||
prompt += `→ Sous-titre H3 (4-6 mots) spécialisé ${contextAnalysis.mainKeyword}\n`;
|
||||
} else if (missing.type.includes('texte') || missing.type.includes('txt')) {
|
||||
prompt += `→ Thème/sujet pour paragraphe 150 mots sur ${contextAnalysis.mainKeyword}\n`;
|
||||
} else if (missing.type.includes('faq_question')) {
|
||||
prompt += `→ Question client directe sur ${contextAnalysis.mainKeyword} (8-12 mots)\n`;
|
||||
} else if (missing.type.includes('faq_reponse')) {
|
||||
prompt += `→ Thème réponse experte ${contextAnalysis.mainKeyword} (2-4 mots)\n`;
|
||||
} else {
|
||||
prompt += `→ Expression/mot-clé pertinent ${contextAnalysis.mainKeyword}\n`;
|
||||
}
|
||||
prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`;
|
||||
});
|
||||
|
||||
prompt += `\nCONSIGNES:
|
||||
- Reste dans le thème ${contextAnalysis.mainKeyword}
|
||||
- Varie les angles et expressions
|
||||
- Évite répétitions avec mots-clés existants
|
||||
- Précis et pertinents
|
||||
- Thème: ${contextAnalysis.mainKeyword}
|
||||
- Mots-clés SEO naturels
|
||||
- Varie les termes
|
||||
- Évite répétitions
|
||||
|
||||
FORMAT:
|
||||
[${missingElements[0].name}]
|
||||
Expression/mot-clé généré 1
|
||||
mot-clé
|
||||
|
||||
[${missingElements[1] ? missingElements[1].name : 'exemple'}]
|
||||
Expression/mot-clé généré 2
|
||||
mot-clé
|
||||
|
||||
etc...`;
|
||||
|
||||
@ -2596,6 +2592,11 @@ const { logSh } = require('./ErrorReporting');
|
||||
const { tracer } = require('./trace.js');
|
||||
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig');
|
||||
|
||||
// Utilitaire pour les délais
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
|
||||
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection
|
||||
@ -2804,8 +2805,8 @@ async function generateAllContentBase(hierarchy, csvData, aiProvider) {
|
||||
}
|
||||
|
||||
/**
|
||||
* ÉTAPE 2 - Enhancement technique BATCH OPTIMISÉ avec IA configurable
|
||||
* OPTIMISATION : 1 appel extraction + 1 appel enhancement au lieu de 20+
|
||||
* ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
|
||||
* NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
|
||||
*/
|
||||
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
|
||||
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO');
|
||||
@ -2872,96 +2873,96 @@ async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
|
||||
* Analyser un seul élément pour détecter les termes techniques
|
||||
*/
|
||||
async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
|
||||
const contentEntries = Object.keys(baseContents);
|
||||
|
||||
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
|
||||
async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
|
||||
const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.
|
||||
|
||||
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
|
||||
|
||||
CONTENUS À ANALYSER:
|
||||
|
||||
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
|
||||
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
|
||||
CONTENU À ANALYSER:
|
||||
TAG: ${tag}
|
||||
CONTENU: "${content}"
|
||||
|
||||
CONSIGNES:
|
||||
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
|
||||
- Cherche UNIQUEMENT des vrais termes techniques métier/industrie
|
||||
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
|
||||
- Focus: matériaux, procédés, normes, dimensions, technologies
|
||||
- Si aucun terme technique → "AUCUN"
|
||||
- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques
|
||||
|
||||
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
|
||||
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
|
||||
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
|
||||
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance
|
||||
|
||||
FORMAT RÉPONSE EXACT:
|
||||
[1] dibond, impression UV, 3mm OU AUCUN
|
||||
[2] aluminium, fraisage CNC OU AUCUN
|
||||
[3] AUCUN
|
||||
etc... (${contentEntries.length} lignes total)`;
|
||||
RÉPONSE REQUISE:
|
||||
- Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
|
||||
- Si aucun terme technique: "NON"
|
||||
|
||||
EXEMPLE:
|
||||
OUI - termes: aluminium composite, impression numérique, gravure laser`;
|
||||
|
||||
try {
|
||||
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
|
||||
temperature: 0.3,
|
||||
maxTokens: 2000
|
||||
}, csvData.personality);
|
||||
|
||||
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
|
||||
const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
|
||||
|
||||
if (response.toUpperCase().startsWith('OUI')) {
|
||||
// Extraire les termes de la réponse
|
||||
const termsMatch = response.match(/termes:\s*(.+)/i);
|
||||
const terms = termsMatch ? termsMatch[1].trim() : '';
|
||||
logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG');
|
||||
return true;
|
||||
} else {
|
||||
logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
|
||||
logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
|
||||
return false; // En cas d'erreur, on skip l'enhancement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
||||
* Enhancer un seul élément techniquement
|
||||
*/
|
||||
async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
|
||||
if (elementsNeedingEnhancement.length === 0) return {};
|
||||
async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
|
||||
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: ${csvData.mc0} - Secteur: signalétique/impression
|
||||
|
||||
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
|
||||
CONTEXTE: ${csvData.mc0} - Secteur: Signalétique/impression
|
||||
VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
|
||||
CONTENU À AMÉLIORER:
|
||||
TAG: ${tag}
|
||||
CONTENU: "${content}"
|
||||
|
||||
CONTENUS + TERMES À AMÉLIORER:
|
||||
OBJECTIFS:
|
||||
- Remplace les termes génériques par des termes techniques précis
|
||||
- Ajoute des spécifications techniques réalistes
|
||||
- Maintient le même style et longueur
|
||||
- Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes
|
||||
|
||||
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
||||
CONTENU ACTUEL: "${item.content}"
|
||||
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
|
||||
EXEMPLE DE TRANSFORMATION:
|
||||
"matériaux haute performance" → "dibond 3mm ou aluminium composite"
|
||||
"impression moderne" → "impression UV haute définition"
|
||||
"fixation solide" → "fixation par chevilles inox Ø6mm"
|
||||
|
||||
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)`;
|
||||
CONTRAINTES:
|
||||
- GARDE la même structure
|
||||
- MÊME longueur approximative
|
||||
- Style cohérent avec l'original
|
||||
- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;
|
||||
|
||||
try {
|
||||
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
|
||||
temperature: 0.4,
|
||||
maxTokens: 5000 // Plus large pour batch total
|
||||
}, csvData.personality);
|
||||
|
||||
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
|
||||
|
||||
const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
|
||||
return enhancedContent.trim();
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
|
||||
logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
|
||||
return content; // En cas d'erreur, on retourne le contenu original
|
||||
}
|
||||
}
|
||||
|
||||
// ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
||||
*/
|
||||
// FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel
|
||||
|
||||
/**
|
||||
* ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable
|
||||
*/
|
||||
@ -3015,7 +3016,8 @@ async function enhanceAllTransitions(baseContents, csvData, aiProvider) {
|
||||
|
||||
const batchTransitionsPrompt = `MISSION: Améliore UNIQUEMENT les transitions et fluidité de ces contenus.
|
||||
|
||||
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style})
|
||||
CONTEXTE: Article SEO professionnel pour site web commercial
|
||||
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} adapté web)
|
||||
CONNECTEURS PRÉFÉRÉS: ${csvData.personality?.connecteursPref}
|
||||
|
||||
CONTENUS:
|
||||
@ -3093,9 +3095,10 @@ async function enhanceAllPersonalityStyle(baseContents, csvData, aiProvider) {
|
||||
|
||||
const batchStylePrompt = `MISSION: Adapte UNIQUEMENT le style de ces contenus selon ${personality.nom}.
|
||||
|
||||
CONTEXTE: Finalisation article SEO pour site e-commerce professionnel
|
||||
PERSONNALITÉ: ${personality.nom}
|
||||
DESCRIPTION: ${personality.description}
|
||||
STYLE CIBLE: ${personality.style}
|
||||
STYLE CIBLE: ${personality.style} adapté au web professionnel
|
||||
VOCABULAIRE: ${personality.vocabulairePref}
|
||||
CONNECTEURS: ${personality.connecteursPref}
|
||||
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
|
||||
@ -3149,9 +3152,8 @@ etc...`;
|
||||
/**
|
||||
* Sleep function replacement for Utilities.sleep
|
||||
*/
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12
|
||||
|
||||
/**
|
||||
* RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
|
||||
@ -3187,32 +3189,33 @@ async function generateFAQPairsRestored(faqPairs, csvData, aiProvider) {
|
||||
function createBatchFAQPairsPrompt(faqPairs, csvData) {
|
||||
const personality = csvData.personality;
|
||||
|
||||
let prompt = `PERSONNALITÉ: ${personality.nom} | ${personality.description}
|
||||
STYLE: ${personality.style}
|
||||
VOCABULAIRE: ${personality.vocabulairePref}
|
||||
CONNECTEURS: ${personality.connecteursPref}
|
||||
NIVEAU TECHNIQUE: ${personality.niveauTechnique}
|
||||
let prompt = `=== 1. CONTEXTE ===
|
||||
Entreprise: Autocollant.fr - signalétique personnalisée
|
||||
Sujet: ${csvData.mc0}
|
||||
Section: FAQ pour article SEO commercial
|
||||
|
||||
GÉNÈRE ${faqPairs.length} PAIRES FAQ COHÉRENTES pour ${csvData.mc0}:
|
||||
=== 2. PERSONNALITÉ ===
|
||||
Rédacteur: ${personality.nom}
|
||||
Style: ${personality.style}
|
||||
Ton: ${personality.description || 'professionnel'}
|
||||
|
||||
RÈGLES STRICTES:
|
||||
- QUESTIONS: Neutres, directes, langage client naturel (8-15 mots)
|
||||
- RÉPONSES: Style ${personality.style}, vocabulaire ${personality.vocabulairePref} (50-80 mots)
|
||||
- Sujets à couvrir: prix, livraison, personnalisation, installation, durabilité
|
||||
- ÉVITE répétitions excessives et expressions trop familières
|
||||
- Style ${personality.nom} reconnaissable mais PROFESSIONNEL
|
||||
- PAS de messages d'excuse ("je n'ai pas l'information")
|
||||
- RÉPONDS DIRECTEMENT par questions et réponses, sans préfixe
|
||||
=== 3. RÈGLES GÉNÉRALES ===
|
||||
- Questions naturelles de clients
|
||||
- Réponses expertes et rassurantes
|
||||
- Langage professionnel mais accessible
|
||||
- Textes rédigés humainement et de façon authentique
|
||||
- Couvrir: prix, livraison, personnalisation, installation, durabilité
|
||||
- IMPÉRATIF: Respecter strictement les contraintes XML
|
||||
|
||||
=== 4. PAIRES FAQ À GÉNÉRER ===
|
||||
|
||||
PAIRES À GÉNÉRER:
|
||||
`;
|
||||
|
||||
faqPairs.forEach((pair, index) => {
|
||||
const questionTag = pair.question.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
||||
const answerTag = pair.answer.tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '');
|
||||
|
||||
prompt += `${index + 1}. [${questionTag}] + [${answerTag}]
|
||||
Question client sur ${csvData.mc0} → Réponse ${personality.style}
|
||||
prompt += `${index + 1}. [${questionTag}] + [${answerTag}] - Paire FAQ naturelle
|
||||
`;
|
||||
});
|
||||
|
||||
@ -3533,10 +3536,25 @@ function findAssociatedTitle(textElement, existingResults) {
|
||||
function createBatchBasePrompt(elements, type, csvData, existingResults = {}) {
|
||||
const personality = csvData.personality;
|
||||
|
||||
let prompt = `RÉDACTEUR: ${personality.nom} | Style: ${personality.style}
|
||||
SUJET: ${csvData.mc0}
|
||||
let prompt = `=== 1. CONTEXTE ===
|
||||
Entreprise: Autocollant.fr - signalétique personnalisée
|
||||
Sujet: ${csvData.mc0}
|
||||
Type d'article: SEO professionnel pour site commercial
|
||||
|
||||
${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${elements.length} ${type.toUpperCase()}S PROFESSIONNELS`}:
|
||||
=== 2. PERSONNALITÉ ===
|
||||
Rédacteur: ${personality.nom}
|
||||
Style: ${personality.style}
|
||||
Ton: ${personality.description || 'professionnel'}
|
||||
|
||||
=== 3. RÈGLES GÉNÉRALES ===
|
||||
- Contenu SEO optimisé
|
||||
- Langage naturel et fluide
|
||||
- Éviter répétitions
|
||||
- Pas de références techniques dans le contenu
|
||||
- Textes rédigés humainement et de façon authentique
|
||||
- IMPÉRATIF: Respecter strictement les contraintes XML (nombre de mots, etc.)
|
||||
|
||||
=== 4. ÉLÉMENTS À GÉNÉRER ===
|
||||
`;
|
||||
|
||||
// AJOUTER CONTEXTE DES TITRES POUR LES TEXTES
|
||||
@ -3548,7 +3566,7 @@ ${type === 'titre' ? 'GÉNÈRE DES TITRES COURTS ET IMPACTANTS' : `GÉNÈRE ${el
|
||||
|
||||
if (generatedTitles.length > 0) {
|
||||
prompt += `
|
||||
CONTEXTE - TITRES GÉNÉRÉS:
|
||||
Titres existants pour contexte:
|
||||
${generatedTitles.join('\n')}
|
||||
|
||||
`;
|
||||
@ -3560,34 +3578,33 @@ ${generatedTitles.join('\n')}
|
||||
|
||||
prompt += `${index + 1}. [${cleanTag}] `;
|
||||
|
||||
// INSTRUCTIONS SPÉCIFIQUES ET COURTES PAR TYPE
|
||||
// INSTRUCTIONS PROPRES PAR ÉLÉMENT
|
||||
if (type === 'titre') {
|
||||
if (elementInfo.element.type === 'titre_h1') {
|
||||
prompt += `CRÉER UN TITRE H1 PRINCIPAL (8-12 mots) sur "${csvData.t0}" - NE PAS écrire "Titre_H1_1"\n`;
|
||||
prompt += `Titre principal accrocheur\n`;
|
||||
} else if (elementInfo.element.type === 'titre_h2') {
|
||||
prompt += `CRÉER UN TITRE H2 SECTION (6-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_H2_X"\n`;
|
||||
prompt += `Titre de section engageant\n`;
|
||||
} else if (elementInfo.element.type === 'titre_h3') {
|
||||
prompt += `CRÉER UN TITRE H3 SOUS-SECTION (4-8 mots) - NE PAS écrire "Titre_H3_X"\n`;
|
||||
prompt += `Sous-titre spécialisé\n`;
|
||||
} else {
|
||||
prompt += `CRÉER UN TITRE ACCROCHEUR (4-10 mots) sur "${csvData.mc0}" - NE PAS écrire "Titre_"\n`;
|
||||
prompt += `Titre pertinent\n`;
|
||||
}
|
||||
} else if (type === 'texte') {
|
||||
const wordCount = elementInfo.element.name && elementInfo.element.name.includes('H2') ? '150' : '100';
|
||||
prompt += `Paragraphe ${wordCount} mots, style ${personality.style}\n`;
|
||||
prompt += `Paragraphe informatif\n`;
|
||||
|
||||
// ASSOCIER LE TITRE CORRESPONDANT AUTOMATIQUEMENT
|
||||
const associatedTitle = findAssociatedTitle(elementInfo, existingResults);
|
||||
if (associatedTitle) {
|
||||
prompt += ` Développe le titre: "${associatedTitle}"\n`;
|
||||
prompt += ` Contexte: "${associatedTitle}"\n`;
|
||||
}
|
||||
|
||||
if (elementInfo.element.resolvedContent) {
|
||||
prompt += ` Thème: "${elementInfo.element.resolvedContent}"\n`;
|
||||
prompt += ` Angle: "${elementInfo.element.resolvedContent}"\n`;
|
||||
}
|
||||
} else if (type === 'intro') {
|
||||
prompt += `Introduction 80-100 mots, ton accueillant\n`;
|
||||
prompt += `Introduction engageante\n`;
|
||||
} else {
|
||||
prompt += `Contenu pertinent pour ${csvData.mc0}\n`;
|
||||
prompt += `Contenu pertinent\n`;
|
||||
}
|
||||
});
|
||||
|
||||
@ -3597,15 +3614,16 @@ ${generatedTitles.join('\n')}
|
||||
- Phrases: ${personality.longueurPhrases}
|
||||
- Niveau technique: ${personality.niveauTechnique}
|
||||
|
||||
CONSIGNES STRICTES:
|
||||
- RESPECTE le style ${personality.style} de ${personality.nom} mais RESTE PROFESSIONNEL
|
||||
- INTERDICTION ABSOLUE: "du coup", "bon", "alors", "franchement", "nickel", "tip-top", "costaud" en excès
|
||||
- VARIE les connecteurs: ${personality.connecteursPref}
|
||||
- POUR LES TITRES: SEULEMENT le titre réel, JAMAIS de référence "Titre_H1_1" ou "Titre_H2_7"
|
||||
- EXEMPLE TITRE: "Plaques personnalisées résistantes aux intempéries" PAS "Titre_H2_1"
|
||||
- RÉPONDS DIRECTEMENT par le contenu demandé, SANS introduction ni nom de tag
|
||||
- PAS de message d'excuse du type "je n'ai pas l'information"
|
||||
- CONTENU cohérent et professionnel, évite la sur-familiarité
|
||||
CONSIGNES STRICTES POUR ARTICLE SEO:
|
||||
- CONTEXTE: Article professionnel pour site e-commerce, destiné aux clients potentiels
|
||||
- STYLE: ${personality.style} de ${personality.nom} mais ADAPTÉ au web professionnel
|
||||
- INTERDICTION ABSOLUE: expressions trop familières répétées ("du coup", "bon", "franchement", "nickel", "tip-top")
|
||||
- VOCABULAIRE: Mélange expertise technique + accessibilité client
|
||||
- SEO: Utilise naturellement "${csvData.mc0}" et termes associés
|
||||
- POUR LES TITRES: Titre SEO attractif UNIQUEMENT, JAMAIS "Titre_H1_1" ou "Titre_H2_7"
|
||||
- EXEMPLE TITRE: "Plaques personnalisées résistantes : guide complet 2024"
|
||||
- CONTENU: Informatif, rassurant, incite à l'achat SANS être trop commercial
|
||||
- RÉPONDS DIRECTEMENT par le contenu web demandé, SANS préfixe
|
||||
|
||||
FORMAT DE RÉPONSE ${type === 'titre' ? '(TITRES UNIQUEMENT)' : ''}:
|
||||
[${elements[0].tag.replace(/\|/g, '').replace(/[{}]/g, '').replace(/<\/?strong>/g, '')}]
|
||||
@ -3696,107 +3714,11 @@ function cleanXMLTagsFromContent(content) {
|
||||
|
||||
// ============= PARSING FUNCTIONS =============
|
||||
|
||||
/**
|
||||
* Parser réponse extraction termes
|
||||
*/
|
||||
function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
|
||||
const results = [];
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
const parsedItems = {};
|
||||
// FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel
|
||||
|
||||
// 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;
|
||||
}
|
||||
// FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser réponse batch transitions/style (format identique)
|
||||
*/
|
||||
function parseTechnicalBatchResponse(response, chunk) {
|
||||
const results = {};
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
let index = 0;
|
||||
|
||||
while ((match = regex.exec(response)) && index < chunk.length) {
|
||||
const content = match[2].trim();
|
||||
results[chunk[index].tag] = content;
|
||||
index++;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
// Placeholder pour les fonctions de parsing conservées qui suivent
|
||||
|
||||
function parseTransitionsBatchResponse(response, chunk) {
|
||||
const results = {};
|
||||
@ -4088,6 +4010,182 @@ RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
|
||||
*/
|
||||
async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
|
||||
const contentEntries = Object.keys(baseContents);
|
||||
|
||||
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
|
||||
|
||||
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
|
||||
|
||||
CONTENUS À ANALYSER:
|
||||
|
||||
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
|
||||
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
|
||||
|
||||
CONSIGNES:
|
||||
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
|
||||
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
|
||||
- Focus: matériaux, procédés, normes, dimensions, technologies
|
||||
- Si aucun terme technique → "AUCUN"
|
||||
|
||||
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
|
||||
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
|
||||
|
||||
FORMAT RÉPONSE EXACT:
|
||||
[1] dibond, impression UV, 3mm OU AUCUN
|
||||
[2] aluminium, fraisage CNC OU AUCUN
|
||||
[3] AUCUN
|
||||
etc... (${contentEntries.length} lignes total)`;
|
||||
|
||||
try {
|
||||
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
|
||||
temperature: 0.3,
|
||||
maxTokens: 2000
|
||||
}, csvData.personality);
|
||||
|
||||
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
|
||||
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
||||
*/
|
||||
async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
|
||||
if (elementsNeedingEnhancement.length === 0) return {};
|
||||
|
||||
const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus.
|
||||
|
||||
CONTEXTE: Article SEO pour site e-commerce de signalétique
|
||||
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} web professionnel)
|
||||
SUJET: ${csvData.mc0} - Secteur: Signalétique/impression
|
||||
VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
|
||||
|
||||
CONTENUS + TERMES À AMÉLIORER:
|
||||
|
||||
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
||||
CONTENU ACTUEL: "${item.content}"
|
||||
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
|
||||
|
||||
CONSIGNES STRICTES:
|
||||
- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom}
|
||||
- GARDE la même longueur, structure et ton
|
||||
- Intègre naturellement les termes techniques listés
|
||||
- NE CHANGE PAS le fond du message ni le style personnel
|
||||
- Utilise un vocabulaire expert mais accessible
|
||||
- ÉVITE les répétitions excessives
|
||||
- RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique}
|
||||
- Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA
|
||||
|
||||
FORMAT RÉPONSE:
|
||||
[1] Contenu avec amélioration technique selon ${csvData.personality?.nom}
|
||||
[2] Contenu avec amélioration technique selon ${csvData.personality?.nom}
|
||||
etc... (${elementsNeedingEnhancement.length} éléments total)`;
|
||||
|
||||
try {
|
||||
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
|
||||
temperature: 0.4,
|
||||
maxTokens: 5000 // Plus large pour batch total
|
||||
}, csvData.personality);
|
||||
|
||||
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
|
||||
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser réponse extraction termes
|
||||
*/
|
||||
function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
|
||||
const results = [];
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
const parsedItems = {};
|
||||
|
||||
// Parser la réponse
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
const index = parseInt(match[1]) - 1; // Convertir en 0-indexé
|
||||
const termsText = match[2].trim();
|
||||
parsedItems[index] = termsText;
|
||||
}
|
||||
|
||||
// Mapper aux éléments
|
||||
contentEntries.forEach((tag, index) => {
|
||||
const termsText = parsedItems[index] || 'AUCUN';
|
||||
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
|
||||
|
||||
const technicalTerms = hasTerms ?
|
||||
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
|
||||
[];
|
||||
|
||||
results.push({
|
||||
tag: tag,
|
||||
content: baseContents[tag],
|
||||
technicalTerms: technicalTerms,
|
||||
needsEnhancement: hasTerms && technicalTerms.length > 0
|
||||
});
|
||||
|
||||
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG');
|
||||
});
|
||||
|
||||
const enhancementCount = results.filter(r => r.needsEnhancement).length;
|
||||
logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO');
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser réponse enhancement technique
|
||||
*/
|
||||
function parseTechnicalEnhancementBatchResponse(response, elementsNeedingEnhancement) {
|
||||
const results = {};
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
let index = 0;
|
||||
|
||||
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
|
||||
let content = match[2].trim();
|
||||
const element = elementsNeedingEnhancement[index];
|
||||
|
||||
// NOUVEAU: Appliquer le nettoyage XML
|
||||
content = cleanXMLTagsFromContent(content);
|
||||
|
||||
if (content && content.length > 10) {
|
||||
results[element.tag] = content;
|
||||
logSh(`✅ Enhanced [${element.tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
|
||||
} else {
|
||||
// Fallback si contenu invalide après nettoyage
|
||||
results[element.tag] = element.content;
|
||||
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide après nettoyage`, 'WARNING');
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
// Vérifier si on a bien tout parsé
|
||||
if (Object.keys(results).length < elementsNeedingEnhancement.length) {
|
||||
logSh(`⚠️ Parsing partiel: ${Object.keys(results).length}/${elementsNeedingEnhancement.length}`, 'WARNING');
|
||||
|
||||
// Compléter avec contenu original pour les manquants
|
||||
elementsNeedingEnhancement.forEach(element => {
|
||||
if (!results[element.tag]) {
|
||||
results[element.tag] = element.content;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ============= EXPORTS =============
|
||||
|
||||
module.exports = {
|
||||
@ -4110,7 +4208,11 @@ module.exports = {
|
||||
generateFAQPairsRestored,
|
||||
createBatchFAQPairsPrompt,
|
||||
parseFAQPairsResponse,
|
||||
cleanFAQInstructions
|
||||
cleanFAQInstructions,
|
||||
extractAllTechnicalTermsBatch,
|
||||
enhanceAllElementsTechnicalBatch,
|
||||
parseAllTechnicalTermsResponse,
|
||||
parseTechnicalEnhancementBatchResponse
|
||||
};
|
||||
|
||||
/*
|
||||
@ -5107,6 +5209,31 @@ const SHEET_CONFIG = {
|
||||
keyFile: './seo-generator-470715-85d4a971c1af.json'
|
||||
};
|
||||
|
||||
async function deployArticle({ path, html, dryRun = false, ...rest }) {
|
||||
if (!path || typeof html !== 'string') {
|
||||
const err = new Error('deployArticle: invalid payload (requires {path, html})');
|
||||
err.code = 'E_PAYLOAD';
|
||||
throw err;
|
||||
}
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
length: html.length,
|
||||
path,
|
||||
meta: rest || {}
|
||||
};
|
||||
}
|
||||
// --- Impl réelle à toi ici (upload DO Spaces / API / SSH etc.) ---
|
||||
// return await realDeploy({ path, html, ...rest });
|
||||
|
||||
// Placeholder pour ne pas casser l'appel si pas encore implémenté
|
||||
return { ok: true, dryRun: false, path, length: html.length };
|
||||
}
|
||||
|
||||
module.exports.deployArticle = module.exports.deployArticle || deployArticle;
|
||||
|
||||
|
||||
// ============= TRIGGER PRINCIPAL REMPLACÉ PAR WEBHOOK/API =============
|
||||
|
||||
/**
|
||||
@ -5549,22 +5676,42 @@ let logViewerLaunched = false;
|
||||
* Lancer le log viewer dans Edge
|
||||
*/
|
||||
function launchLogViewer() {
|
||||
if (logViewerLaunched) return;
|
||||
if (logViewerLaunched || process.env.NODE_ENV === 'test') return;
|
||||
|
||||
try {
|
||||
const logViewerPath = path.join(__dirname, '..', 'logs-viewer.html');
|
||||
const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html');
|
||||
const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`;
|
||||
|
||||
// Lancer Edge avec l'URL du fichier
|
||||
const edgeProcess = spawn('cmd', ['/c', 'start', 'msedge', fileUrl], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
// Détecter l'environnement et adapter la commande
|
||||
const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP;
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
if (isWindows && !isWSL) {
|
||||
// Windows natif
|
||||
const edgeProcess = spawn('cmd', ['/c', 'start', 'msedge', fileUrl], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
edgeProcess.unref();
|
||||
} else if (isWSL) {
|
||||
// WSL - utiliser cmd.exe via /mnt/c/Windows/System32/
|
||||
const edgeProcess = spawn('/mnt/c/Windows/System32/cmd.exe', ['/c', 'start', 'msedge', fileUrl], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
edgeProcess.unref();
|
||||
} else {
|
||||
// Linux/Mac - essayer xdg-open ou open
|
||||
const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
||||
const browserProcess = spawn(command, [fileUrl], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
browserProcess.unref();
|
||||
}
|
||||
|
||||
edgeProcess.unref();
|
||||
logViewerLaunched = true;
|
||||
|
||||
logSh('🌐 Log viewer ouvert dans Edge', 'INFO');
|
||||
logSh('🌐 Log viewer lancé', 'INFO');
|
||||
} catch (error) {
|
||||
logSh(`⚠️ Impossible d'ouvrir le log viewer: ${error.message}`, 'WARNING');
|
||||
}
|
||||
|
||||
@ -43,12 +43,13 @@ const fileDest = pino.destination({
|
||||
});
|
||||
tee.pipe(fileDest);
|
||||
|
||||
// Custom levels for Pino to include TRACE and PROMPT
|
||||
// Custom levels for Pino to include TRACE, PROMPT, and LLM
|
||||
const customLevels = {
|
||||
trace: 5, // Below debug (10)
|
||||
debug: 10,
|
||||
info: 20,
|
||||
prompt: 25, // New level for prompts (between info and warn)
|
||||
llm: 26, // New level for LLM interactions (between prompt and warn)
|
||||
warn: 30,
|
||||
error: 40,
|
||||
fatal: 50
|
||||
@ -164,6 +165,9 @@ async function logSh(message, level = 'INFO') {
|
||||
case 'prompt':
|
||||
logger.prompt(traceData, message);
|
||||
break;
|
||||
case 'llm':
|
||||
logger.llm(traceData, message);
|
||||
break;
|
||||
default:
|
||||
logger.info(traceData, message);
|
||||
}
|
||||
|
||||
@ -125,7 +125,10 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
|
||||
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
|
||||
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
|
||||
logSh(prompt, 'PROMPT');
|
||||
logSh(`===== FIN PROMPT ${llmProvider.toUpperCase()} (${personality?.nom || 'AUCUNE'}) =====\n`, 'PROMPT');
|
||||
|
||||
// 📤 LOG LLM REQUEST COMPLET
|
||||
logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM');
|
||||
logSh(prompt, 'LLM');
|
||||
|
||||
// Préparer la requête selon le provider
|
||||
const requestData = buildRequestData(llmProvider, prompt, options, personality);
|
||||
@ -136,8 +139,12 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
|
||||
// Parser la réponse selon le format du provider
|
||||
const content = parseResponse(llmProvider, response);
|
||||
|
||||
// 📥 LOG LLM RESPONSE COMPLET
|
||||
logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM');
|
||||
logSh(content, 'LLM');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms: "${content.substring(0, 150)}${content.length > 150 ? '...' : ''}"`, 'INFO');
|
||||
logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');
|
||||
|
||||
// Enregistrer les stats d'usage
|
||||
await recordUsageStats(llmProvider, prompt.length, content.length, duration);
|
||||
|
||||
@ -30,7 +30,7 @@ function launchLogViewer() {
|
||||
if (logViewerLaunched || process.env.NODE_ENV === 'test') return;
|
||||
|
||||
try {
|
||||
const logViewerPath = path.join(__dirname, '..', 'logs-viewer.html');
|
||||
const logViewerPath = path.join(__dirname, '..', 'tools', 'logs-viewer.html');
|
||||
const fileUrl = `file:///${logViewerPath.replace(/\\/g, '/')}`;
|
||||
|
||||
// Détecter l'environnement et adapter la commande
|
||||
|
||||
@ -8,6 +8,11 @@ const { logSh } = require('./ErrorReporting');
|
||||
const { tracer } = require('./trace.js');
|
||||
const { selectMultiplePersonalitiesWithAI, getPersonalities } = require('./BrainConfig');
|
||||
|
||||
// Utilitaire pour les délais
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE APPROCHE - Multi-Personnalités Batch Enhancement
|
||||
* 4 personnalités différentes utilisées dans le pipeline pour maximum d'anti-détection
|
||||
@ -216,8 +221,8 @@ async function generateAllContentBase(hierarchy, csvData, aiProvider) {
|
||||
}
|
||||
|
||||
/**
|
||||
* ÉTAPE 2 - Enhancement technique BATCH OPTIMISÉ avec IA configurable
|
||||
* OPTIMISATION : 1 appel extraction + 1 appel enhancement au lieu de 20+
|
||||
* ÉTAPE 2 - Enhancement technique ÉLÉMENT PAR ÉLÉMENT avec IA configurable
|
||||
* NOUVEAU : Traitement individuel pour fiabilité maximale et debug précis
|
||||
*/
|
||||
async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
|
||||
logSh('🔧 === DÉBUT ENHANCEMENT TECHNIQUE ===', 'INFO');
|
||||
@ -284,97 +289,96 @@ async function enhanceAllTechnicalTerms(baseContents, csvData, aiProvider) {
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
|
||||
* Analyser un seul élément pour détecter les termes techniques
|
||||
*/
|
||||
async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
|
||||
const contentEntries = Object.keys(baseContents);
|
||||
|
||||
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
|
||||
async function analyzeSingleElementTechnicalTerms(tag, content, csvData, aiProvider) {
|
||||
const prompt = `MISSION: Analyser ce contenu et déterminer s'il contient des termes techniques.
|
||||
|
||||
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
|
||||
|
||||
CONTENUS À ANALYSER:
|
||||
|
||||
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
|
||||
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
|
||||
CONTENU À ANALYSER:
|
||||
TAG: ${tag}
|
||||
CONTENU: "${content}"
|
||||
|
||||
CONSIGNES:
|
||||
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
|
||||
- Cherche UNIQUEMENT des vrais termes techniques métier/industrie
|
||||
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
|
||||
- Focus: matériaux, procédés, normes, dimensions, technologies
|
||||
- Si aucun terme technique → "AUCUN"
|
||||
- Focus: matériaux, procédés, normes, dimensions, technologies spécifiques
|
||||
|
||||
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
|
||||
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
|
||||
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé, anodisation
|
||||
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique, haute performance
|
||||
|
||||
FORMAT RÉPONSE EXACT:
|
||||
[1] dibond, impression UV, 3mm OU AUCUN
|
||||
[2] aluminium, fraisage CNC OU AUCUN
|
||||
[3] AUCUN
|
||||
etc... (${contentEntries.length} lignes total)`;
|
||||
RÉPONSE REQUISE:
|
||||
- Si termes techniques trouvés: "OUI - termes: [liste des termes séparés par virgules]"
|
||||
- Si aucun terme technique: "NON"
|
||||
|
||||
EXEMPLE:
|
||||
OUI - termes: aluminium composite, impression numérique, gravure laser`;
|
||||
|
||||
try {
|
||||
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
|
||||
temperature: 0.3,
|
||||
maxTokens: 2000
|
||||
}, csvData.personality);
|
||||
|
||||
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
|
||||
const response = await callLLM(aiProvider, prompt, { temperature: 0.3 });
|
||||
|
||||
if (response.toUpperCase().startsWith('OUI')) {
|
||||
// Extraire les termes de la réponse
|
||||
const termsMatch = response.match(/termes:\s*(.+)/i);
|
||||
const terms = termsMatch ? termsMatch[1].trim() : '';
|
||||
logSh(`✅ [${tag}] Termes techniques détectés: ${terms}`, 'DEBUG');
|
||||
return true;
|
||||
} else {
|
||||
logSh(`⏭️ [${tag}] Pas de termes techniques`, 'DEBUG');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
|
||||
logSh(`❌ ERREUR analyse ${tag}: ${error.message}`, 'ERROR');
|
||||
return false; // En cas d'erreur, on skip l'enhancement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
||||
* Enhancer un seul élément techniquement
|
||||
*/
|
||||
async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
|
||||
if (elementsNeedingEnhancement.length === 0) return {};
|
||||
async function enhanceSingleElementTechnical(tag, content, csvData, aiProvider) {
|
||||
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: ${csvData.mc0} - Secteur: signalétique/impression
|
||||
|
||||
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}
|
||||
CONTENU À AMÉLIORER:
|
||||
TAG: ${tag}
|
||||
CONTENU: "${content}"
|
||||
|
||||
CONTENUS + TERMES À AMÉLIORER:
|
||||
OBJECTIFS:
|
||||
- Remplace les termes génériques par des termes techniques précis
|
||||
- Ajoute des spécifications techniques réalistes
|
||||
- Maintient le même style et longueur
|
||||
- Intègre naturellement: matériaux (dibond, aluminium composite), procédés (impression UV, gravure laser), dimensions, normes
|
||||
|
||||
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
||||
CONTENU ACTUEL: "${item.content}"
|
||||
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
|
||||
EXEMPLE DE TRANSFORMATION:
|
||||
"matériaux haute performance" → "dibond 3mm ou aluminium composite"
|
||||
"impression moderne" → "impression UV haute définition"
|
||||
"fixation solide" → "fixation par chevilles inox Ø6mm"
|
||||
|
||||
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)`;
|
||||
CONTRAINTES:
|
||||
- GARDE la même structure
|
||||
- MÊME longueur approximative
|
||||
- Style cohérent avec l'original
|
||||
- RÉPONDS DIRECTEMENT par le contenu amélioré, sans préfixe`;
|
||||
|
||||
try {
|
||||
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
|
||||
temperature: 0.4,
|
||||
maxTokens: 5000 // Plus large pour batch total
|
||||
}, csvData.personality);
|
||||
|
||||
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
|
||||
|
||||
const enhancedContent = await callLLM(aiProvider, prompt, { temperature: 0.7 });
|
||||
return enhancedContent.trim();
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
|
||||
logSh(`❌ ERREUR enhancement ${tag}: ${error.message}`, 'ERROR');
|
||||
return content; // En cas d'erreur, on retourne le contenu original
|
||||
}
|
||||
}
|
||||
|
||||
// ANCIENNES FONCTIONS BATCH SUPPRIMÉES - REMPLACÉES PAR TRAITEMENT INDIVIDUEL
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
||||
*/
|
||||
// FONCTION SUPPRIMÉE : enhanceAllElementsTechnicalBatch() - Remplacée par traitement individuel
|
||||
|
||||
/**
|
||||
* ÉTAPE 3 - Enhancement transitions BATCH avec IA configurable
|
||||
*/
|
||||
@ -564,9 +568,8 @@ etc...`;
|
||||
/**
|
||||
* Sleep function replacement for Utilities.sleep
|
||||
*/
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// FONCTION SUPPRIMÉE : sleep() dupliquée - déjà définie ligne 12
|
||||
|
||||
/**
|
||||
* RESTAURÉ DEPUIS .GS : Génération des paires FAQ cohérentes
|
||||
@ -1127,107 +1130,11 @@ function cleanXMLTagsFromContent(content) {
|
||||
|
||||
// ============= PARSING FUNCTIONS =============
|
||||
|
||||
/**
|
||||
* Parser réponse extraction termes
|
||||
*/
|
||||
function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
|
||||
const results = [];
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
const parsedItems = {};
|
||||
// FONCTION SUPPRIMÉE : parseAllTechnicalTermsResponse() - Parser batch défaillant remplacé par traitement individuel
|
||||
|
||||
// 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;
|
||||
}
|
||||
// FONCTIONS SUPPRIMÉES : parseTechnicalEnhancementBatchResponse() et parseTechnicalBatchResponse() - Remplacées par traitement individuel
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser réponse batch transitions/style (format identique)
|
||||
*/
|
||||
function parseTechnicalBatchResponse(response, chunk) {
|
||||
const results = {};
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
let index = 0;
|
||||
|
||||
while ((match = regex.exec(response)) && index < chunk.length) {
|
||||
const content = match[2].trim();
|
||||
results[chunk[index].tag] = content;
|
||||
index++;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
// Placeholder pour les fonctions de parsing conservées qui suivent
|
||||
|
||||
function parseTransitionsBatchResponse(response, chunk) {
|
||||
const results = {};
|
||||
@ -1519,6 +1426,182 @@ RÉPONDS UNIQUEMENT PAR LE CONTENU, sans présentation.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Extraction batch TOUS les termes techniques
|
||||
*/
|
||||
async function extractAllTechnicalTermsBatch(baseContents, csvData, aiProvider) {
|
||||
const contentEntries = Object.keys(baseContents);
|
||||
|
||||
const batchAnalysisPrompt = `MISSION: Analyser ces ${contentEntries.length} contenus et identifier leurs termes techniques.
|
||||
|
||||
CONTEXTE: ${csvData.mc0} - Secteur: signalétique/impression
|
||||
|
||||
CONTENUS À ANALYSER:
|
||||
|
||||
${contentEntries.map((tag, i) => `[${i + 1}] TAG: ${tag}
|
||||
CONTENU: "${baseContents[tag]}"`).join('\n\n')}
|
||||
|
||||
CONSIGNES:
|
||||
- Identifie UNIQUEMENT les vrais termes techniques métier/industrie
|
||||
- Évite mots génériques (qualité, service, pratique, personnalisé, etc.)
|
||||
- Focus: matériaux, procédés, normes, dimensions, technologies
|
||||
- Si aucun terme technique → "AUCUN"
|
||||
|
||||
EXEMPLES VALIDES: dibond, impression UV, fraisage CNC, épaisseur 3mm, aluminium brossé
|
||||
EXEMPLES INVALIDES: durable, pratique, personnalisé, moderne, esthétique
|
||||
|
||||
FORMAT RÉPONSE EXACT:
|
||||
[1] dibond, impression UV, 3mm OU AUCUN
|
||||
[2] aluminium, fraisage CNC OU AUCUN
|
||||
[3] AUCUN
|
||||
etc... (${contentEntries.length} lignes total)`;
|
||||
|
||||
try {
|
||||
const analysisResponse = await callLLM(aiProvider, batchAnalysisPrompt, {
|
||||
temperature: 0.3,
|
||||
maxTokens: 2000
|
||||
}, csvData.personality);
|
||||
|
||||
return parseAllTechnicalTermsResponse(analysisResponse, baseContents, contentEntries);
|
||||
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Extraction termes techniques batch échouée: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Analyse termes techniques impossible - arrêt du workflow: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOUVELLE FONCTION : Enhancement batch TOUS les éléments
|
||||
*/
|
||||
async function enhanceAllElementsTechnicalBatch(elementsNeedingEnhancement, csvData, aiProvider) {
|
||||
if (elementsNeedingEnhancement.length === 0) return {};
|
||||
|
||||
const batchEnhancementPrompt = `MISSION: Améliore UNIQUEMENT la précision technique de ces ${elementsNeedingEnhancement.length} contenus.
|
||||
|
||||
CONTEXTE: Article SEO pour site e-commerce de signalétique
|
||||
PERSONNALITÉ: ${csvData.personality?.nom} (${csvData.personality?.style} web professionnel)
|
||||
SUJET: ${csvData.mc0} - Secteur: Signalétique/impression
|
||||
VOCABULAIRE PRÉFÉRÉ: ${csvData.personality?.vocabulairePref}
|
||||
|
||||
CONTENUS + TERMES À AMÉLIORER:
|
||||
|
||||
${elementsNeedingEnhancement.map((item, i) => `[${i + 1}] TAG: ${item.tag}
|
||||
CONTENU ACTUEL: "${item.content}"
|
||||
TERMES TECHNIQUES À INTÉGRER: ${item.technicalTerms.join(', ')}`).join('\n\n')}
|
||||
|
||||
CONSIGNES STRICTES:
|
||||
- Améliore UNIQUEMENT la précision technique, garde le style ${csvData.personality?.nom}
|
||||
- GARDE la même longueur, structure et ton
|
||||
- Intègre naturellement les termes techniques listés
|
||||
- NE CHANGE PAS le fond du message ni le style personnel
|
||||
- Utilise un vocabulaire expert mais accessible
|
||||
- ÉVITE les répétitions excessives
|
||||
- RESPECTE le niveau technique: ${csvData.personality?.niveauTechnique}
|
||||
- Termes techniques secteur: dibond, aluminium, impression UV, fraisage, épaisseur, PMMA
|
||||
|
||||
FORMAT RÉPONSE:
|
||||
[1] Contenu avec amélioration technique selon ${csvData.personality?.nom}
|
||||
[2] Contenu avec amélioration technique selon ${csvData.personality?.nom}
|
||||
etc... (${elementsNeedingEnhancement.length} éléments total)`;
|
||||
|
||||
try {
|
||||
const enhanced = await callLLM(aiProvider, batchEnhancementPrompt, {
|
||||
temperature: 0.4,
|
||||
maxTokens: 5000 // Plus large pour batch total
|
||||
}, csvData.personality);
|
||||
|
||||
return parseTechnicalEnhancementBatchResponse(enhanced, elementsNeedingEnhancement);
|
||||
|
||||
} catch (error) {
|
||||
logSh(`❌ FATAL: Enhancement technique batch échoué: ${error.message}`, 'ERROR');
|
||||
throw new Error(`FATAL: Enhancement technique batch impossible - arrêt du workflow: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser réponse extraction termes
|
||||
*/
|
||||
function parseAllTechnicalTermsResponse(response, baseContents, contentEntries) {
|
||||
const results = [];
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
const parsedItems = {};
|
||||
|
||||
// Parser la réponse
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
const index = parseInt(match[1]) - 1; // Convertir en 0-indexé
|
||||
const termsText = match[2].trim();
|
||||
parsedItems[index] = termsText;
|
||||
}
|
||||
|
||||
// Mapper aux éléments
|
||||
contentEntries.forEach((tag, index) => {
|
||||
const termsText = parsedItems[index] || 'AUCUN';
|
||||
const hasTerms = !termsText.toUpperCase().includes('AUCUN');
|
||||
|
||||
const technicalTerms = hasTerms ?
|
||||
termsText.split(',').map(t => t.trim()).filter(t => t.length > 0) :
|
||||
[];
|
||||
|
||||
results.push({
|
||||
tag: tag,
|
||||
content: baseContents[tag],
|
||||
technicalTerms: technicalTerms,
|
||||
needsEnhancement: hasTerms && technicalTerms.length > 0
|
||||
});
|
||||
|
||||
logSh(`🔍 [${tag}]: ${hasTerms ? technicalTerms.join(', ') : 'pas de termes techniques'}`, 'DEBUG');
|
||||
});
|
||||
|
||||
const enhancementCount = results.filter(r => r.needsEnhancement).length;
|
||||
logSh(`📊 Analyse terminée: ${enhancementCount}/${contentEntries.length} éléments ont besoin d'enhancement`, 'INFO');
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser réponse enhancement technique
|
||||
*/
|
||||
function parseTechnicalEnhancementBatchResponse(response, elementsNeedingEnhancement) {
|
||||
const results = {};
|
||||
const regex = /\[(\d+)\]\s*([^[]*?)(?=\[\d+\]|$)/gs;
|
||||
let match;
|
||||
let index = 0;
|
||||
|
||||
while ((match = regex.exec(response)) && index < elementsNeedingEnhancement.length) {
|
||||
let content = match[2].trim();
|
||||
const element = elementsNeedingEnhancement[index];
|
||||
|
||||
// NOUVEAU: Appliquer le nettoyage XML
|
||||
content = cleanXMLTagsFromContent(content);
|
||||
|
||||
if (content && content.length > 10) {
|
||||
results[element.tag] = content;
|
||||
logSh(`✅ Enhanced [${element.tag}]: "${content.substring(0, 100)}..."`, 'DEBUG');
|
||||
} else {
|
||||
// Fallback si contenu invalide après nettoyage
|
||||
results[element.tag] = element.content;
|
||||
logSh(`⚠️ Fallback [${element.tag}]: contenu invalide après nettoyage`, 'WARNING');
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
// Vérifier si on a bien tout parsé
|
||||
if (Object.keys(results).length < elementsNeedingEnhancement.length) {
|
||||
logSh(`⚠️ Parsing partiel: ${Object.keys(results).length}/${elementsNeedingEnhancement.length}`, 'WARNING');
|
||||
|
||||
// Compléter avec contenu original pour les manquants
|
||||
elementsNeedingEnhancement.forEach(element => {
|
||||
if (!results[element.tag]) {
|
||||
results[element.tag] = element.content;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ============= EXPORTS =============
|
||||
|
||||
module.exports = {
|
||||
@ -1541,5 +1624,9 @@ module.exports = {
|
||||
generateFAQPairsRestored,
|
||||
createBatchFAQPairsPrompt,
|
||||
parseFAQPairsResponse,
|
||||
cleanFAQInstructions
|
||||
cleanFAQInstructions,
|
||||
extractAllTechnicalTermsBatch,
|
||||
enhanceAllElementsTechnicalBatch,
|
||||
parseAllTechnicalTermsResponse,
|
||||
parseTechnicalEnhancementBatchResponse
|
||||
};
|
||||
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 toInt(v,d){const n=parseInt(v,10);return Number.isFinite(n)?n:d;}
|
||||
|
||||
const LEVEL_MAP_NUM = {10:'TRACE',20:'DEBUG',25:'PROMPT',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'};
|
||||
const LEVEL_MAP_NUM = {10:'TRACE',20:'DEBUG',25:'PROMPT',26:'LLM',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'};
|
||||
function normLevel(v){
|
||||
if (v==null) return 'UNKNOWN';
|
||||
if (typeof v==='number') return LEVEL_MAP_NUM[v]||String(v);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user