// ======================================== // SMART TOUCH CORE - Orchestrateur SelectiveSmartTouch // Responsabilité: Orchestration complète Analyse → Améliorations ciblées // Architecture: Analyse intelligente PUIS améliorations précises (contrôle total) // ======================================== const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { SmartAnalysisLayer } = require('./SmartAnalysisLayer'); const { SmartTechnicalLayer } = require('./SmartTechnicalLayer'); const { SmartStyleLayer } = require('./SmartStyleLayer'); const { SmartReadabilityLayer } = require('./SmartReadabilityLayer'); const { GlobalBudgetManager } = require('./GlobalBudgetManager'); // ✅ NOUVEAU: Budget global /** * SMART TOUCH CORE * Orchestrateur principal: Analyse → Technical → Style → Readability (ciblé) */ class SmartTouchCore { constructor() { this.name = 'SelectiveSmartTouch'; // Instancier les layers this.analysisLayer = new SmartAnalysisLayer(); this.technicalLayer = new SmartTechnicalLayer(); this.styleLayer = new SmartStyleLayer(); this.readabilityLayer = new SmartReadabilityLayer(); } /** * APPLIQUER SMART TOUCH COMPLET * @param {object} content - Map {tag: texte} * @param {object} config - Configuration * @returns {object} - Résultat avec stats détaillées */ async apply(content, config = {}) { return await tracer.run('SmartTouchCore.apply()', async () => { const { mode = 'full', // 'analysis_only', 'technical_only', 'style_only', 'readability_only', 'full' intensity = 1.0, csvData = null, llmProvider = 'gpt-4o-mini', // ✅ LLM à utiliser (extrait du pipeline config) skipAnalysis = false, // Si true, applique sans analyser (mode legacy) layersOrder = ['technical', 'style', 'readability'], // Ordre d'application personnalisable charsPerExpression = 4000 // ✅ NOUVEAU: Caractères par expression familière (configurable) } = config; await tracer.annotate({ selectiveSmartTouch: true, mode, intensity, elementsCount: Object.keys(content).length, personality: csvData?.personality?.nom }); const startTime = Date.now(); logSh(`🧠 SELECTIVE SMART TOUCH START: ${Object.keys(content).length} éléments | Mode: ${mode}`, 'INFO'); try { let currentContent = { ...content }; const stats = { mode, analysisResults: {}, layersApplied: [], totalModifications: 0, elementsProcessed: Object.keys(content).length, elementsImproved: 0, duration: 0 }; // === ✅ FIX: INITIALISER BUDGET GLOBAL AVANT TOUT (scope global) === const budgetConfig = { charsPerExpression: config.charsPerExpression || 4000 // Configurable via pipeline }; const budgetManager = new GlobalBudgetManager(csvData?.personality, currentContent, budgetConfig); const allTags = Object.keys(currentContent); budgetManager.distributeRandomly(allTags); logSh(`💰 Budget global initialisé: ${JSON.stringify(budgetManager.budget)}`, 'INFO'); logSh(`🎲 Budget distribué sur ${allTags.length} tags`, 'INFO'); // ======================================== // PHASE 1: ANALYSE INTELLIGENTE // ======================================== if (!skipAnalysis) { logSh(`\n📊 === PHASE 1: ANALYSE INTELLIGENTE ===`, 'INFO'); const analysisResults = await this.analysisLayer.analyzeBatch(currentContent, { mc0: csvData?.mc0, personality: csvData?.personality, llmProvider // ✅ Passer LLM à l'analyse batch }); stats.analysisResults = analysisResults; // Résumer analyse const summary = this.analysisLayer.summarizeBatchAnalysis(analysisResults); logSh(` 📋 Résumé analyse: ${summary.needsImprovement}/${summary.totalElements} éléments nécessitent amélioration`, 'INFO'); logSh(` 📊 Score moyen: ${summary.averageScore.toFixed(2)} | Améliorations totales: ${summary.totalImprovements}`, 'INFO'); logSh(` 🎯 Besoins: Technical=${summary.commonIssues.technical} | Style=${summary.commonIssues.style} | Readability=${summary.commonIssues.readability}`, 'INFO'); // Si mode analysis_only, retourner ici if (mode === 'analysis_only') { const duration = Date.now() - startTime; logSh(`✅ SELECTIVE SMART TOUCH (ANALYSIS ONLY) terminé: ${duration}ms`, 'INFO'); return { content: currentContent, stats: { ...stats, duration, analysisOnly: true }, modifications: 0, analysisResults }; } // ======================================== // PHASE 2: AMÉLIORATIONS CIBLÉES // ======================================== logSh(`\n🔧 === PHASE 2: AMÉLIORATIONS CIBLÉES ===`, 'INFO'); // Déterminer quelles couches appliquer const layersToApply = this.determineLayersToApply(mode, layersOrder); // === DÉTECTION CONTEXTE GLOBALE (1 seule fois) === const contentContext = this.analysisLayer.detectContentContext( Object.values(currentContent).join(' '), csvData?.personality ); // === ✅ FIX: SÉLECTIONNER 10% SEGMENTS UNE SEULE FOIS (GLOBAL) === logSh(`\n🎯 === SÉLECTION 10% SEGMENTS GLOBAUX ===`, 'INFO'); const percentageToImprove = intensity * 0.1; const segmentsByTag = {}; // Pré-calculer segments et sélection pour chaque tag UNE SEULE FOIS for (const [tag, text] of Object.entries(currentContent)) { const analysis = analysisResults[tag]; if (!analysis) continue; // Analyser par segments const segments = this.analysisLayer.analyzeBySegments(text, { mc0: csvData?.mc0, personality: csvData?.personality }); // Sélectionner les X% segments les plus faibles const weakestSegments = this.analysisLayer.selectWeakestSegments( segments, percentageToImprove ); segmentsByTag[tag] = { segments, weakestSegments, analysis }; logSh(` 📊 [${tag}] ${segments.length} segments, ${weakestSegments.length} sélectionnés (${(percentageToImprove * 100).toFixed(0)}%)`, 'INFO'); } // === APPLIQUER TOUTES LES LAYERS SUR LES MÊMES SEGMENTS SÉLECTIONNÉS === const improvedTags = new Set(); // ✅ FIX: Tracker les tags améliorés (pas de duplicata) for (const layerName of layersToApply) { const layerStartTime = Date.now(); logSh(`\n 🎯 Couche: ${layerName}`, 'INFO'); let layerModifications = 0; const layerResults = {}; // Appliquer la couche sur chaque élément (en réutilisant les MÊMES segments sélectionnés) for (const [tag, text] of Object.entries(currentContent)) { const tagData = segmentsByTag[tag]; if (!tagData) continue; try { // ✅ Utiliser les segments PRÉ-SÉLECTIONNÉS (pas de nouvelle sélection) const { segments, weakestSegments, analysis } = tagData; // Appliquer amélioration UNIQUEMENT sur segments déjà sélectionnés const result = await this.applyLayerToSegments( layerName, segments, weakestSegments, analysis, { mc0: csvData?.mc0, personality: csvData?.personality, intensity, contentContext, // Passer contexte aux layers llmProvider, // ✅ Passer LLM choisi dans pipeline budgetManager, // ✅ NOUVEAU: Passer budget manager currentTag: tag // ✅ NOUVEAU: Tag actuel pour tracking budget } ); if (!result.skipped && result.content !== text) { currentContent[tag] = result.content; layerModifications += result.modifications || 0; improvedTags.add(tag); // ✅ FIX: Ajouter au Set (pas de duplicata) } layerResults[tag] = result; } catch (error) { logSh(` ❌ [${tag}] Échec ${layerName}: ${error.message}`, 'ERROR'); } } const layerDuration = Date.now() - layerStartTime; stats.layersApplied.push({ name: layerName, modifications: layerModifications, duration: layerDuration }); stats.totalModifications += layerModifications; logSh(` ✅ ${layerName} terminé: ${layerModifications} modifications (${layerDuration}ms)`, 'INFO'); } // ✅ FIX: Mettre à jour le compteur d'éléments améliorés APRÈS toutes les layers stats.elementsImproved = improvedTags.size; } else { // Mode skipAnalysis: appliquer sans analyse (legacy fallback) logSh(`⚠️ Mode skipAnalysis activé: application directe sans analyse préalable`, 'WARNING'); // TODO: Implémenter mode legacy si nécessaire logSh(`❌ Mode skipAnalysis non implémenté pour SmartTouch (requiert analyse)`, 'ERROR'); } // ======================================== // RÉSULTATS FINAUX // ======================================== const duration = Date.now() - startTime; stats.duration = duration; // === ✅ NOUVEAU: Rapport budget final === const budgetReport = budgetManager?.getReport(); if (budgetReport) { stats.budgetReport = budgetReport; logSh(`\n💰 === RAPPORT BUDGET EXPRESSIONS ===`, 'INFO'); logSh(` 📊 Budget total: ${budgetReport.budget.total}`, 'INFO'); logSh(` ✅ Consommé: ${budgetReport.totalConsumed} (${budgetReport.percentageUsed}%)`, 'INFO'); logSh(` 💸 Restant: ${budgetReport.remaining}`, 'INFO'); if (budgetReport.overBudget) { logSh(` ⚠️ DÉPASSEMENT BUDGET !`, 'WARN'); } } logSh(`\n✅ === SELECTIVE SMART TOUCH TERMINÉ ===`, 'INFO'); logSh(` 📊 ${stats.elementsImproved}/${stats.elementsProcessed} éléments améliorés`, 'INFO'); logSh(` 🔄 ${stats.totalModifications} modifications totales`, 'INFO'); logSh(` ⏱️ Durée: ${duration}ms`, 'INFO'); logSh(` 🎯 Couches appliquées: ${stats.layersApplied.map(l => l.name).join(' → ')}`, 'INFO'); await tracer.event('SelectiveSmartTouch terminé', stats); return { content: currentContent, stats, modifications: stats.totalModifications, analysisResults: stats.analysisResults, budgetReport // ✅ NOUVEAU: Inclure rapport budget }; } catch (error) { const duration = Date.now() - startTime; logSh(`❌ SELECTIVE SMART TOUCH ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR'); return { content, // Fallback: contenu original stats: { error: error.message, duration, fallback: true }, modifications: 0, fallback: true }; } }, { content: Object.keys(content), config }); } /** * DÉTERMINER COUCHES À APPLIQUER */ determineLayersToApply(mode, layersOrder) { switch (mode) { case 'technical_only': return ['technical']; case 'style_only': return ['style']; case 'readability_only': return ['readability']; case 'full': default: return layersOrder; // Ordre personnalisable } } /** * APPLIQUER UNE COUCHE SPÉCIFIQUE */ async applyLayer(layerName, content, analysis, context) { switch (layerName) { case 'technical': return await this.technicalLayer.applyTargeted(content, analysis, context); case 'style': return await this.styleLayer.applyTargeted(content, analysis, context); case 'readability': return await this.readabilityLayer.applyTargeted(content, analysis, context); default: throw new Error(`Couche inconnue: ${layerName}`); } } /** * APPLIQUER COUCHE SUR SEGMENTS SÉLECTIONNÉS UNIQUEMENT (10% système) * @param {string} layerName - Nom de la couche * @param {array} allSegments - Tous les segments du texte * @param {array} weakestSegments - Segments sélectionnés à améliorer * @param {object} analysis - Analyse globale * @param {object} context - Contexte * @returns {object} - { content: texte réassemblé, modifications, ... } */ async applyLayerToSegments(layerName, allSegments, weakestSegments, analysis, context) { // Si aucun segment à améliorer, retourner texte original if (weakestSegments.length === 0) { const originalContent = allSegments.map(s => s.content).join(' '); return { content: originalContent, modifications: 0, skipped: true, reason: 'No weak segments identified' }; } // Créer Map des indices des segments à améliorer pour lookup rapide const weakIndices = new Set(weakestSegments.map(s => s.index)); // === AMÉLIORER UNIQUEMENT LES SEGMENTS FAIBLES === const improvedSegments = []; let totalModifications = 0; for (const segment of allSegments) { if (weakIndices.has(segment.index)) { // AMÉLIORER ce segment try { const result = await this.applyLayer(layerName, segment.content, analysis, context); improvedSegments.push({ ...segment, content: result.skipped ? segment.content : result.content, improved: !result.skipped }); totalModifications += result.modifications || 0; } catch (error) { logSh(` ⚠️ Échec amélioration segment ${segment.index}: ${error.message}`, 'WARN'); // Fallback: garder segment original improvedSegments.push({ ...segment, content: segment.content, // ✅ FIX: Copier content original improved: false }); } } else { // GARDER segment intact improvedSegments.push({ ...segment, content: segment.content, // ✅ FIX: Copier content original improved: false }); } } // === RÉASSEMBLER TEXTE COMPLET === const reassembledContent = improvedSegments.map(s => s.content).join(' '); // Nettoyer espaces multiples const cleanedContent = reassembledContent.replace(/\s{2,}/g, ' ').trim(); const improvedCount = improvedSegments.filter(s => s.improved).length; logSh(` ✅ ${improvedCount}/${allSegments.length} segments améliorés (${totalModifications} modifs)`, 'DEBUG'); return { content: cleanedContent, modifications: totalModifications, segmentsImproved: improvedCount, segmentsTotal: allSegments.length, skipped: false }; } /** * MODES DISPONIBLES */ static getAvailableModes() { return [ { name: 'analysis_only', description: 'Analyse uniquement (sans amélioration)', layers: [] }, { name: 'technical_only', description: 'Améliorations techniques ciblées uniquement', layers: ['technical'] }, { name: 'style_only', description: 'Améliorations style ciblées uniquement', layers: ['style'] }, { name: 'readability_only', description: 'Améliorations lisibilité ciblées uniquement', layers: ['readability'] }, { name: 'full', description: 'Analyse + toutes améliorations ciblées (recommandé)', layers: ['technical', 'style', 'readability'] } ]; } } module.exports = { SmartTouchCore };