seo-generator-server/public/pipeline-runner.js
StillHammer 0244521f5c feat(selective-smart-touch): Add intelligent analysis-driven enhancement system + validation spec
## SelectiveSmartTouch (NEW)
- Architecture révolutionnaire: Analyse intelligente → Améliorations ciblées précises
- 5 modules: SmartAnalysisLayer, SmartTechnicalLayer, SmartStyleLayer, SmartReadabilityLayer, SmartTouchCore
- Système 10% segments: amélioration uniquement des segments les plus faibles (intensity-based)
- Détection contexte globale pour prompts adaptatifs multi-secteurs
- Intégration complète dans PipelineExecutor et PipelineDefinition

## Pipeline Validator Spec (NEW)
- Spécification complète système validation qualité par LLM
- 5 critères universels: Qualité, Verbosité, SEO, Répétitions, Naturalité
- Échantillonnage intelligent par filtrage balises (pas XML)
- Évaluation multi-versions avec justifications détaillées
- Coût estimé: ~$1/validation (260 appels LLM)

## Optimizations
- Réduction intensités fullEnhancement (technical 1.0→0.7, style 0.8→0.5)
- Ajout gardes-fous anti-familiarité excessive dans StyleLayer
- Sauvegarde étapes intermédiaires activée par défaut (pipeline-runner)

## Fixes
- Fix typo critique SmartTouchCore.js:110 (determineLayers ToApply → determineLayersToApply)
- Prompts généralisés multi-secteurs (e-commerce, SaaS, services, informatif)

🚀 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 15:01:02 +08:00

382 lines
14 KiB
JavaScript

/**
* Pipeline Runner - Client Side Logic
* Gestion de l'exécution des pipelines sauvegardés
*/
// État global
const state = {
pipelines: [],
selectedPipeline: null,
running: false
};
// ====================
// INITIALIZATION
// ====================
window.onload = async function() {
await loadPipelinesList();
};
// Charger la liste des pipelines
async function loadPipelinesList() {
try {
const response = await fetch('/api/pipeline/list');
const data = await response.json();
if (data.success) {
state.pipelines = data.pipelines;
renderPipelinesDropdown();
}
} catch (error) {
showStatus(`Erreur chargement pipelines: ${error.message}`, 'error');
}
}
// Rendre le dropdown de pipelines
function renderPipelinesDropdown() {
const select = document.getElementById('pipelineSelect');
select.innerHTML = '<option value="">-- Sélectionner un pipeline --</option>';
state.pipelines.forEach(pipeline => {
const option = document.createElement('option');
option.value = pipeline.name;
option.textContent = `${pipeline.name} (${pipeline.steps} étapes, ~${pipeline.estimatedDuration})`;
select.appendChild(option);
});
}
// ====================
// PIPELINE LOADING
// ====================
async function loadPipeline() {
const select = document.getElementById('pipelineSelect');
const pipelineName = select.value;
if (!pipelineName) {
document.getElementById('pipelinePreview').style.display = 'none';
document.getElementById('btnRun').disabled = true;
return;
}
try {
const response = await fetch(`/api/pipeline/${pipelineName}`);
const data = await response.json();
if (data.success) {
state.selectedPipeline = data.pipeline;
displayPipelinePreview(data.pipeline);
document.getElementById('btnRun').disabled = false;
}
} catch (error) {
showStatus(`Erreur chargement pipeline: ${error.message}`, 'error');
}
}
// Afficher la prévisualisation du pipeline
function displayPipelinePreview(pipeline) {
const preview = document.getElementById('pipelinePreview');
preview.style.display = 'block';
document.getElementById('pipelineName').textContent = pipeline.name;
document.getElementById('pipelineDesc').textContent = pipeline.description || 'Pas de description';
document.getElementById('summarySteps').textContent = pipeline.pipeline.length;
// Estimation durée
const estimatedSeconds = pipeline.pipeline.length * 20; // Rough estimate
const minutes = Math.floor(estimatedSeconds / 60);
const seconds = estimatedSeconds % 60;
document.getElementById('summaryDuration').textContent = minutes > 0
? `${minutes}m ${seconds}s`
: `${seconds}s`;
// Liste des étapes
const stepList = document.getElementById('stepList');
stepList.innerHTML = '';
pipeline.pipeline.forEach(step => {
const div = document.createElement('div');
div.className = 'step-item';
div.textContent = `${step.step}. ${step.module} (${step.mode}) - Intensité: ${step.intensity}`;
stepList.appendChild(div);
});
}
// ====================
// PIPELINE EXECUTION
// ====================
async function runPipeline() {
if (!state.selectedPipeline) {
showStatus('Aucun pipeline sélectionné', 'error');
return;
}
if (state.running) {
showStatus('Une exécution est déjà en cours', 'error');
return;
}
const rowNumber = parseInt(document.getElementById('rowNumber').value);
const saveIntermediateSteps = document.getElementById('saveIntermediateSteps').checked;
if (!rowNumber || rowNumber < 2) {
showStatus('Numéro de ligne invalide (minimum 2)', 'error');
return;
}
state.running = true;
document.getElementById('btnRun').disabled = true;
// Show progress section
document.getElementById('progressSection').style.display = 'block';
document.getElementById('progressBar').style.display = 'block';
document.getElementById('progressText').style.display = 'block';
document.getElementById('resultsSection').style.display = 'none';
showStatus('🚀 Exécution du pipeline en cours...', 'loading');
updateProgress(0, 'Initialisation...');
try {
const response = await fetch('/api/pipeline/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pipelineConfig: state.selectedPipeline,
rowNumber: rowNumber,
options: {
saveIntermediateSteps: saveIntermediateSteps
}
})
});
updateProgress(50, 'Traitement en cours...');
const data = await response.json();
updateProgress(100, 'Terminé!');
if (data.success) {
displayResults(data.result);
showStatus('✅ Pipeline exécuté avec succès!', 'success');
} else {
showStatus(`❌ Erreur: ${data.error}`, 'error');
}
} catch (error) {
showStatus(`❌ Erreur exécution: ${error.message}`, 'error');
console.error('Execution error:', error);
} finally {
state.running = false;
document.getElementById('btnRun').disabled = false;
setTimeout(() => {
document.getElementById('progressSection').style.display = 'none';
}, 2000);
}
}
// ====================
// RESULTS DISPLAY
// ====================
function displayResults(result) {
const resultsSection = document.getElementById('resultsSection');
resultsSection.style.display = 'block';
// Log complet des résultats dans la console
console.log('=== RÉSULTAT PIPELINE ===');
console.log('Contenu final:', result.finalContent || result.content);
console.log('Stats:', result.stats);
console.log('Version history:', result.versionHistory);
console.log('Résultat complet:', result);
console.log('========================');
// Stats
document.getElementById('statDuration').textContent =
`${result.stats.totalDuration}ms`;
document.getElementById('statSuccessSteps').textContent =
`${result.stats.successfulSteps}/${result.stats.totalSteps}`;
document.getElementById('statPersonality').textContent =
result.stats.personality || 'N/A';
// Final Content Display
const finalContentContainer = document.getElementById('finalContentContainer');
let rawContent = result.finalContent || result.content || result.organicContent;
// Extraire le texte si c'est un objet
let finalContent;
let isStructuredContent = false;
if (typeof rawContent === 'string') {
finalContent = rawContent;
} else if (rawContent && typeof rawContent === 'object') {
// Vérifier si c'est un contenu structuré (H2_1, H3_2, etc.)
const keys = Object.keys(rawContent);
if (keys.some(k => k.match(/^(H2|H3|P)_\d+$/))) {
isStructuredContent = true;
// Formater le contenu structuré
finalContent = keys
.sort((a, b) => {
// Trier par type (H2, H3, P) puis par numéro
const aMatch = a.match(/^([A-Z]+)_(\d+)$/);
const bMatch = b.match(/^([A-Z]+)_(\d+)$/);
if (!aMatch || !bMatch) return 0;
if (aMatch[1] !== bMatch[1]) return aMatch[1].localeCompare(bMatch[1]);
return parseInt(aMatch[2]) - parseInt(bMatch[2]);
})
.map(key => {
const match = key.match(/^([A-Z0-9]+)_(\d+)$/);
if (match) {
const tag = match[1];
return `[${tag}]\n${rawContent[key]}\n`;
}
return `${key}: ${rawContent[key]}\n`;
})
.join('\n');
} else {
// Si c'est un objet, essayer d'extraire le texte
finalContent = rawContent.text || rawContent.content || rawContent.organicContent || JSON.stringify(rawContent, null, 2);
}
}
if (finalContent) {
finalContentContainer.innerHTML = '';
// Warning si contenu incomplet
const elementCount = Object.keys(rawContent || {}).length;
if (isStructuredContent && elementCount < 30) {
const warningDiv = document.createElement('div');
warningDiv.style.cssText = 'padding: 10px; margin-bottom: 15px; background: #fed7d7; border: 1px solid #f56565; border-radius: 6px; color: #822727;';
warningDiv.innerHTML = `⚠️ <strong>Génération incomplète:</strong> ${elementCount} éléments générés (attendu ~33). Vérifiez les logs pour plus de détails.`;
finalContentContainer.appendChild(warningDiv);
}
// Créer un élément pre pour préserver le formatage
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'white-space: pre-wrap; line-height: 1.6; color: var(--text-dark); font-size: 14px;';
contentDiv.textContent = finalContent;
// Ajouter un bouton pour copier
const copyBtn = document.createElement('button');
copyBtn.textContent = '📋 Copier le contenu';
copyBtn.style.cssText = 'margin-bottom: 15px; padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;';
copyBtn.onclick = () => {
navigator.clipboard.writeText(finalContent);
copyBtn.textContent = '✓ Copié!';
setTimeout(() => { copyBtn.textContent = '📋 Copier le contenu'; }, 2000);
};
finalContentContainer.appendChild(copyBtn);
finalContentContainer.appendChild(contentDiv);
// Ajouter les métadonnées si disponibles
if (result.stats) {
const metaDiv = document.createElement('div');
metaDiv.style.cssText = 'margin-top: 15px; padding: 10px; background: white; border-radius: 6px; font-size: 12px; color: var(--text-light);';
const contentLength = finalContent.length;
const wordCount = finalContent.split(/\s+/).length;
metaDiv.innerHTML = `<strong>Métadonnées:</strong> ${contentLength} caractères, ~${wordCount} mots`;
finalContentContainer.appendChild(metaDiv);
}
} else {
finalContentContainer.innerHTML = '<p style="color: var(--warning);">⚠️ Aucun contenu final disponible dans le résultat</p>';
}
// Version History
const versionHistoryContainer = document.getElementById('versionHistory');
versionHistoryContainer.innerHTML = '';
if (result.versionHistory && result.versionHistory.length > 0) {
result.versionHistory.forEach(version => {
const div = document.createElement('div');
div.style.cssText = 'padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border-left: 4px solid var(--success);';
const versionLabel = document.createElement('strong');
versionLabel.textContent = `Version ${version.version}`;
versionLabel.style.color = 'var(--primary)';
const articleId = document.createElement('span');
articleId.textContent = ` - Article ID: ${version.articleId}`;
articleId.style.color = 'var(--text-dark)';
const modifications = document.createElement('span');
modifications.textContent = ` - ${version.modifications || 0} modifications`;
modifications.style.color = 'var(--text-light)';
modifications.style.fontSize = '13px';
modifications.style.marginLeft = '10px';
div.appendChild(versionLabel);
div.appendChild(articleId);
div.appendChild(modifications);
versionHistoryContainer.appendChild(div);
});
} else {
versionHistoryContainer.innerHTML = '<p style="color: var(--text-light);">Aucune version sauvegardée</p>';
}
// Google Sheets Link
if (result.gsheetsLink) {
document.getElementById('gsheetsLinkContainer').style.display = 'block';
document.getElementById('gsheetsLink').href = result.gsheetsLink;
} else {
document.getElementById('gsheetsLinkContainer').style.display = 'none';
}
// Execution log
const logContainer = document.getElementById('executionLog');
logContainer.innerHTML = '';
if (result.executionLog && result.executionLog.length > 0) {
result.executionLog.forEach(logEntry => {
const div = document.createElement('div');
div.className = `log-entry ${logEntry.success ? 'log-success' : 'log-error'}`;
const status = logEntry.success ? '✓' : '✗';
const text = `${status} Étape ${logEntry.step}: ${logEntry.module} (${logEntry.mode}) ` +
`- ${logEntry.duration}ms`;
if (logEntry.modifications !== undefined) {
div.textContent = text + ` - ${logEntry.modifications} modifs`;
} else {
div.textContent = text;
}
if (!logEntry.success && logEntry.error) {
div.textContent += ` - Erreur: ${logEntry.error}`;
}
logContainer.appendChild(div);
});
} else {
logContainer.textContent = 'Aucun log d\'exécution disponible';
}
}
// ====================
// HELPERS
// ====================
function updateProgress(percentage, text) {
document.getElementById('progressFill').style.width = percentage + '%';
document.getElementById('progressText').textContent = text;
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${type}`;
status.style.display = 'block';
if (type !== 'loading') {
setTimeout(() => {
status.style.display = 'none';
}, 5000);
}
}