seo-generator-server/public/step-by-step.html

1147 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SEO Generator - Step by Step</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
}
.container {
display: grid;
grid-template-columns: 320px 1fr;
grid-template-rows: auto 1fr auto;
gap: 20px;
padding: 20px;
max-width: 1600px;
margin: 0 auto;
min-height: 100vh;
}
.header {
grid-column: 1 / -1;
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
text-align: center;
}
.header h1 {
color: #2d3748;
margin-bottom: 10px;
}
.header .subtitle {
color: #718096;
font-size: 14px;
}
.session-info {
background: #e2e8f0;
padding: 10px;
border-radius: 8px;
margin-top: 15px;
font-size: 12px;
color: #4a5568;
}
.left-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
.panel {
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.panel h2 {
color: #2d3748;
margin-bottom: 15px;
font-size: 1.2em;
display: flex;
align-items: center;
gap: 8px;
}
/* INPUT SECTION */
.input-group {
margin-bottom: 15px;
}
.input-group label {
display: block;
font-weight: 600;
margin-bottom: 6px;
color: #4a5568;
font-size: 13px;
}
.input-group input,
.input-group textarea,
.input-group select {
width: 100%;
padding: 10px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 13px;
transition: border-color 0.2s;
}
.input-group input:focus,
.input-group textarea:focus,
.input-group select:focus {
outline: none;
border-color: #667eea;
}
.input-group textarea {
height: 60px;
resize: vertical;
}
/* CONTRÔLES */
.step-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.step-btn {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 13px;
font-weight: 500;
}
.step-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.step-btn.pending {
background: #f7fafc;
color: #718096;
}
.step-btn.executing {
background: #ffd700;
color: #744210;
animation: pulse 1.5s infinite;
}
.step-btn.completed {
background: #c6f6d5;
color: #22543d;
}
.step-btn.error {
background: #fed7d7;
color: #742a2a;
}
.step-btn .step-info {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
}
.step-btn .step-name {
font-weight: 600;
}
.step-btn .step-desc {
font-size: 11px;
opacity: 0.8;
margin-top: 2px;
}
.step-btn .step-stats {
font-size: 10px;
opacity: 0.7;
text-align: right;
}
.global-controls {
margin-top: 15px;
display: flex;
flex-direction: column;
gap: 8px;
}
.btn {
padding: 10px 15px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 13px;
transition: all 0.2s;
}
.btn.primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn.secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn.warning {
background: #fed7d7;
color: #742a2a;
}
.btn:hover {
transform: translateY(-1px);
}
/* RÉSULTATS */
.results-panel {
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow-y: auto;
max-height: 70vh;
}
.step-results {
display: flex;
flex-direction: column;
gap: 20px;
}
.step-result {
border: 2px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
}
.step-result.active {
border-color: #667eea;
}
.step-result.completed {
border-color: #48bb78;
}
.step-result.error {
border-color: #f56565;
}
.result-header {
background: #f7fafc;
padding: 15px 20px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.result-header h3 {
color: #2d3748;
font-size: 14px;
margin: 0;
}
.result-status {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
.result-status.pending {
background: #e2e8f0;
color: #718096;
}
.result-status.executing {
background: #ffd700;
color: #744210;
}
.result-status.completed {
background: #c6f6d5;
color: #22543d;
}
.result-status.error {
background: #fed7d7;
color: #742a2a;
}
.result-content {
padding: 20px;
}
.format-toggle {
margin-bottom: 15px;
display: flex;
gap: 5px;
}
.format-btn {
padding: 6px 12px;
border: 1px solid #e2e8f0;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
.format-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.content-output {
background: #f8f9fa;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
.content-output.tag-format .tag {
color: #667eea;
font-weight: bold;
}
.result-stats {
margin-top: 15px;
padding: 10px;
background: #f0f4f8;
border-radius: 6px;
display: flex;
gap: 15px;
font-size: 11px;
}
.stat {
display: flex;
align-items: center;
gap: 4px;
color: #4a5568;
}
/* STATS GLOBALES */
.stats-panel {
grid-column: 1 / -1;
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.stat-card {
background: #f8f9fa;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 15px;
}
.stat-card h4 {
color: #2d3748;
margin-bottom: 10px;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.stat-card ul {
list-style: none;
}
.stat-card li {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 12px;
color: #4a5568;
}
.stat-card .stat-value {
font-weight: 600;
color: #2d3748;
}
/* ANIMATIONS */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading::after {
content: "";
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid currentColor;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
margin-left: 8px;
}
/* RESPONSIVE */
@media (max-width: 1200px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto;
}
.left-panel {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.left-panel {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER -->
<div class="header">
<h1>🎯 SEO Generator - Step by Step</h1>
<div class="subtitle">Interface de test modulaire avec contrôle granulaire</div>
<div class="session-info" id="sessionInfo" style="display: none;">
<strong>Session:</strong> <span id="sessionId">-</span> |
<strong>Status:</strong> <span id="sessionStatus">-</span> |
<strong>Étape:</strong> <span id="currentStepInfo">0/4</span>
</div>
</div>
<!-- PANNEAU GAUCHE -->
<div class="left-panel">
<!-- INPUTS PERSONNALISÉS -->
<div class="panel">
<h2>🎯 Configuration</h2>
<div class="input-group">
<label for="mc0">Mot-clé principal (MC0)</label>
<input type="text" id="mc0" placeholder="ex: plaque personnalisée" value="plaque personnalisée">
</div>
<div class="input-group">
<label for="t0">Titre cible (T0)</label>
<input type="text" id="t0" placeholder="ex: Créer une plaque personnalisée" value="Créer une plaque personnalisée unique">
</div>
<div class="input-group">
<label for="mcPlus1">Mots-clés secondaires</label>
<textarea id="mcPlus1" placeholder="plaque gravée,plaque métal,plaque bois">plaque gravée,plaque métal,plaque bois,plaque acrylique</textarea>
</div>
<div class="input-group">
<label for="personality">Personnalité IA</label>
<select id="personality">
<option value="random">🎲 Aléatoire</option>
<option value="marc" selected>Marc (technique)</option>
<option value="sophie">Sophie (créatif)</option>
<option value="laurent">Laurent (commercial)</option>
</select>
</div>
<button class="btn primary" onclick="initSession()" id="initBtn">
🚀 Initialiser Session
</button>
</div>
<!-- CONTRÔLES STEP-BY-STEP -->
<div class="panel">
<h2>🎮 Contrôles</h2>
<div class="step-buttons" id="stepButtons">
<!-- Généré dynamiquement -->
</div>
<div class="global-controls">
<button class="btn secondary" onclick="executeAll()" id="executeAllBtn" disabled>
▶️ Tout Exécuter
</button>
<button class="btn warning" onclick="resetSession()" id="resetBtn" disabled>
🔄 Reset Session
</button>
<button class="btn secondary" onclick="exportResults()" id="exportBtn" disabled>
💾 Export JSON
</button>
</div>
</div>
</div>
<!-- PANNEAU RÉSULTATS -->
<div class="results-panel">
<h2>📊 Résultats par Étape</h2>
<div class="step-results" id="stepResults">
<div style="text-align: center; color: #718096; margin-top: 50px;">
Initialisez une session pour commencer
</div>
</div>
</div>
<!-- STATS GLOBALES -->
<div class="stats-panel">
<h2>📈 Statistiques Détaillées</h2>
<div class="stats-grid">
<div class="stat-card">
<h4>⏱️ Performance</h4>
<ul>
<li>Durée totale: <span class="stat-value" id="totalDuration">0ms</span></li>
<li>Étape la plus lente: <span class="stat-value" id="slowestStep">-</span></li>
<li>Moyenne par étape: <span class="stat-value" id="avgDuration">0ms</span></li>
</ul>
</div>
<div class="stat-card">
<h4>🤖 Utilisation LLM</h4>
<ul>
<li>Tokens utilisés: <span class="stat-value" id="totalTokens">0</span></li>
<li>Appels LLM: <span class="stat-value" id="totalLLMCalls">0</span></li>
<li>Provider principal: <span class="stat-value" id="mainProvider">-</span></li>
</ul>
</div>
<div class="stat-card">
<h4>💰 Coûts</h4>
<ul>
<li>Coût total: <span class="stat-value" id="totalCost">$0.00</span></li>
<li>Coût moyen/étape: <span class="stat-value" id="avgCost">$0.00</span></li>
<li>Système le plus cher: <span class="stat-value" id="mostExpensive">-</span></li>
</ul>
</div>
<div class="stat-card">
<h4>✅ Statut Session</h4>
<ul>
<li>Étapes complétées: <span class="stat-value" id="completedSteps">0</span></li>
<li>Taux de succès: <span class="stat-value" id="successRate">0%</span></li>
<li>Dernière activité: <span class="stat-value" id="lastActivity">-</span></li>
</ul>
</div>
</div>
</div>
</div>
<script>
// Variables globales
let currentSessionId = null;
let currentSession = null;
let stepResultsData = {};
// ==============================================
// INITIALISATION
// ==============================================
document.addEventListener('DOMContentLoaded', function() {
console.log('🎯 Step-by-Step Interface initialisée');
});
// ==============================================
// GESTION SESSION
// ==============================================
async function initSession() {
try {
const initBtn = document.getElementById('initBtn');
initBtn.disabled = true;
initBtn.innerHTML = '⏳ Initialisation...';
const inputData = {
mc0: document.getElementById('mc0').value || 'plaque personnalisée',
t0: document.getElementById('t0').value || 'Créer une plaque personnalisée',
mcPlus1: document.getElementById('mcPlus1').value || '',
personality: document.getElementById('personality').value || 'random'
};
console.log('🚀 Initialisation session avec:', inputData);
const response = await fetch('/api/step-by-step/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(inputData)
});
const data = await response.json();
if (data.success) {
currentSessionId = data.sessionId;
currentSession = {
id: data.sessionId,
inputData: data.inputData,
steps: data.steps
};
updateSessionInfo();
generateStepButtons(data.steps);
generateStepResults(data.steps);
enableControls();
console.log('✅ Session initialisée:', currentSessionId);
} else {
throw new Error(data.message || 'Erreur initialisation');
}
} catch (error) {
console.error('❌ Erreur init session:', error);
alert('Erreur initialisation: ' + error.message);
} finally {
const initBtn = document.getElementById('initBtn');
initBtn.disabled = false;
initBtn.innerHTML = '🚀 Initialiser Session';
}
}
async function resetSession() {
if (!currentSessionId || !confirm('Êtes-vous sûr de vouloir reset la session ?')) {
return;
}
try {
const response = await fetch('/api/step-by-step/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: currentSessionId })
});
const data = await response.json();
if (data.success) {
// Reset l'interface
generateStepButtons(data.steps);
generateStepResults(data.steps);
stepResultsData = {};
updateGlobalStats();
console.log('🔄 Session reset');
} else {
throw new Error(data.message || 'Erreur reset');
}
} catch (error) {
console.error('❌ Erreur reset:', error);
alert('Erreur reset: ' + error.message);
}
}
// ==============================================
// EXÉCUTION ÉTAPES
// ==============================================
async function executeStep(stepId) {
if (!currentSessionId) {
alert('Veuillez d\'abord initialiser une session');
return;
}
try {
console.log(`🚀 Exécution étape ${stepId}`);
// Marquer l'étape comme en cours
updateStepStatus(stepId, 'executing');
const response = await fetch('/api/step-by-step/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: currentSessionId,
stepId: stepId,
options: {} // TODO: permettre configuration
})
});
const data = await response.json();
if (data.success) {
// Stocker le résultat
stepResultsData[stepId] = data;
// Vérifier s'il y a des warnings de debug
if (data.result && data.result.debugWarning) {
console.warn(`⚠️ Warning étape ${stepId}:`, data.result.debugWarning);
displayStepWarning(stepId, data.result.debugWarning);
}
// Mettre à jour l'interface
updateStepStatus(stepId, 'completed');
displayStepResult(stepId, data);
updateGlobalStats();
console.log(`✅ Étape ${stepId} complétée`);
} else {
updateStepStatus(stepId, 'error');
displayStepError(stepId, data.message || 'Erreur inconnue');
console.error(`❌ Erreur étape ${stepId}:`, data.message);
}
} catch (error) {
updateStepStatus(stepId, 'error');
displayStepError(stepId, error.message);
console.error(`❌ Erreur étape ${stepId}:`, error);
}
}
async function executeAll() {
if (!currentSessionId) {
alert('Veuillez d\'abord initialiser une session');
return;
}
const steps = currentSession.steps;
for (const step of steps) {
await executeStep(step.id);
// Petit délai entre les étapes
await new Promise(resolve => setTimeout(resolve, 500));
}
}
// ==============================================
// INTERFACE DYNAMIQUE
// ==============================================
function updateSessionInfo() {
const sessionInfo = document.getElementById('sessionInfo');
const sessionIdEl = document.getElementById('sessionId');
const sessionStatusEl = document.getElementById('sessionStatus');
if (currentSessionId) {
sessionInfo.style.display = 'block';
sessionIdEl.textContent = currentSessionId.substring(0, 8) + '...';
sessionStatusEl.textContent = 'active';
}
}
function generateStepButtons(steps) {
const container = document.getElementById('stepButtons');
container.innerHTML = '';
steps.forEach(step => {
const button = document.createElement('div');
button.className = 'step-btn pending';
button.setAttribute('data-step', step.id);
button.onclick = () => executeStep(step.id);
button.innerHTML = `
<div class="step-info">
<div class="step-name">${step.id}️⃣ ${step.name}</div>
<div class="step-desc">${step.description}</div>
</div>
<div class="step-stats" id="stepStats${step.id}">
En attente
</div>
`;
container.appendChild(button);
});
}
function generateStepResults(steps) {
const container = document.getElementById('stepResults');
container.innerHTML = '';
steps.forEach(step => {
const resultDiv = document.createElement('div');
resultDiv.className = 'step-result';
resultDiv.setAttribute('data-step', step.id);
resultDiv.innerHTML = `
<div class="result-header">
<h3>${step.id}️⃣ ${step.name}</h3>
<div class="result-status pending" id="resultStatus${step.id}">En attente</div>
</div>
<div class="result-content" id="resultContent${step.id}" style="display: none;">
<div class="format-toggle">
<button class="format-btn active" onclick="toggleFormat(${step.id}, 'tag')">[Tag] Format</button>
<button class="format-btn" onclick="toggleFormat(${step.id}, 'xml')">XML Format</button>
</div>
<div class="content-output" id="contentOutput${step.id}">
<!-- Résultat ici -->
</div>
<div class="result-stats" id="resultStats${step.id}">
<!-- Stats ici -->
</div>
</div>
`;
container.appendChild(resultDiv);
});
}
function updateStepStatus(stepId, status) {
// Mettre à jour le bouton
const stepBtn = document.querySelector(`[data-step="${stepId}"]`);
if (stepBtn) {
stepBtn.className = `step-btn ${status}`;
}
// Mettre à jour le statut dans les résultats
const resultStatus = document.getElementById(`resultStatus${stepId}`);
if (resultStatus) {
resultStatus.className = `result-status ${status}`;
resultStatus.textContent = {
pending: 'En attente',
executing: 'En cours...',
completed: 'Complété',
error: 'Erreur'
}[status] || status;
}
// Ajouter l'animation loading si nécessaire
const stepStats = document.getElementById(`stepStats${stepId}`);
if (stepStats) {
if (status === 'executing') {
stepStats.innerHTML = '<span class="loading">En cours</span>';
} else if (status === 'pending') {
stepStats.textContent = 'En attente';
}
}
}
function displayStepResult(stepId, data) {
const contentDiv = document.getElementById(`resultContent${stepId}`);
const outputDiv = document.getElementById(`contentOutput${stepId}`);
const statsDiv = document.getElementById(`resultStats${stepId}`);
// Afficher le contenu
contentDiv.style.display = 'block';
// Afficher le résultat avec before/after si disponible
let contentHtml = '';
if (data.result && data.result.beforeAfter) {
// Mode avant/après
contentHtml = '<div class="before-after-container" style="display: flex; gap: 15px; margin-bottom: 15px;">';
// Section AVANT
contentHtml += '<div class="before-section" style="flex: 1; background: #fef3c7; border-radius: 8px; padding: 12px;">';
contentHtml += '<h4 style="margin: 0 0 8px 0; color: #92400e; font-size: 12px;">🔤 AVANT</h4>';
contentHtml += '<div style="font-size: 11px; color: #78350f;">';
contentHtml += formatContentForDisplay(formatContentForTag(data.result.beforeAfter.before), 'compact');
contentHtml += '</div></div>';
// Section APRÈS
contentHtml += '<div class="after-section" style="flex: 1; background: #dcfce7; border-radius: 8px; padding: 12px;">';
contentHtml += '<h4 style="margin: 0 0 8px 0; color: #166534; font-size: 12px;">✨ APRÈS</h4>';
contentHtml += '<div style="font-size: 11px; color: #14532d;">';
contentHtml += formatContentForDisplay(formatContentForTag(data.result.beforeAfter.after), 'compact');
contentHtml += '</div></div>';
contentHtml += '</div>';
// Résultat final complet en bas
if (data.result && data.result.formatted) {
contentHtml += '<div class="final-result" style="margin-top: 15px; padding-top: 15px; border-top: 2px solid #e5e7eb;">';
contentHtml += '<h4 style="margin: 0 0 8px 0; color: #374151; font-size: 12px;">📋 RÉSULTAT FINAL</h4>';
contentHtml += formatContentForDisplay(data.result.formatted, 'tag');
contentHtml += '</div>';
}
} else if (data.result && data.result.formatted) {
// Mode normal (pas de before/after)
contentHtml = formatContentForDisplay(data.result.formatted, 'tag');
} else {
contentHtml = 'Pas de contenu généré';
}
outputDiv.innerHTML = contentHtml;
// Afficher les stats détaillées avec phases
let statsHtml = '';
if (data.stats) {
statsHtml += `
<div class="stat">🕒 ${data.stats.duration}ms</div>
<div class="stat">🎯 ${data.stats.tokensUsed || 0} tokens</div>
<div class="stat">💰 $${(data.stats.cost || 0).toFixed(4)}</div>
<div class="stat">🤖 ${data.stats.system}</div>
`;
}
// Afficher détails des phases si présentes (pour selective enhancement)
if (data.result && data.result.phases) {
statsHtml += '<div class="phases-detail" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e2e8f0;">';
statsHtml += '<h4 style="margin: 0 0 10px 0; font-size: 12px; color: #4a5568;">📋 Détail des Phases:</h4>';
// Phase 1: Génération Initiale
if (data.result.phases.initialGeneration) {
const phase = data.result.phases.initialGeneration;
statsHtml += `
<div class="phase-item" style="margin-bottom: 8px; padding: 8px; background: #f0f9ff; border-radius: 6px;">
<div style="font-weight: 600; font-size: 11px; color: #1e40af;">🎯 Phase 1: Génération Initiale</div>
<div style="font-size: 10px; color: #64748b; margin-top: 2px;">
${phase.generated}/${phase.total} éléments • ${phase.duration}ms • ${phase.llmProvider}
</div>
</div>
`;
}
// Phase 2: Enhancement Sélectif
if (data.result.phases.selectiveEnhancement) {
const phase = data.result.phases.selectiveEnhancement;
statsHtml += `
<div class="phase-item" style="margin-bottom: 8px; padding: 8px; background: #f0fdf4; border-radius: 6px;">
<div style="font-weight: 600; font-size: 11px; color: #16a34a;">⚡ Phase 2: Enhancement Sélectif</div>
<div style="font-size: 10px; color: #64748b; margin-top: 2px;">
${phase.totalEnhancements} améliorations • ${phase.totalDuration}ms
</div>
`;
// Détail des sous-étapes d'enhancement
if (phase.steps) {
phase.steps.forEach((step, index) => {
const stepEmoji = step.name === 'technical' ? '🔧' :
step.name === 'transitions' ? '🔗' :
step.name === 'style' ? '🎨' : '⚙️';
const stepName = step.name === 'technical' ? 'Technique' :
step.name === 'transitions' ? 'Transitions' :
step.name === 'style' ? 'Style' : step.name;
statsHtml += `
<div style="font-size: 9px; color: #64748b; margin-left: 10px; margin-top: 2px;">
${stepEmoji} ${stepName}: ${step.elementsEnhanced || 0} éléments • ${step.duration}ms • ${step.llm}
</div>
`;
});
}
statsHtml += '</div>';
}
statsHtml += '</div>';
}
// Afficher détails des appels LLM
if (data.result && data.result.llmCalls && data.result.llmCalls.length > 0) {
statsHtml += '<div class="llm-calls" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e2e8f0;">';
statsHtml += '<h4 style="margin: 0 0 10px 0; font-size: 12px; color: #4a5568;">🤖 Appels LLM:</h4>';
data.result.llmCalls.forEach((call, index) => {
const providerEmoji = call.provider === 'claude' ? '🟣' :
call.provider === 'gpt4' ? '🟢' :
call.provider === 'gemini' ? '🔵' :
call.provider === 'mistral' ? '🟠' : '🤖';
const phaseName = call.phase ? ` (${call.phase})` : '';
statsHtml += `
<div class="llm-call" style="margin-bottom: 6px; padding: 6px; background: #f8fafc; border-radius: 4px;">
<div style="font-size: 10px; font-weight: 500; color: #374151;">
${providerEmoji} ${call.provider.toUpperCase()}${phaseName}
</div>
<div style="font-size: 9px; color: #6b7280;">
${call.tokens || 0} tokens • $${(call.cost || 0).toFixed(4)}
${call.error ? ' • ❌ ' + call.error : ''}
</div>
</div>
`;
});
statsHtml += '</div>';
}
if (statsDiv) {
statsDiv.innerHTML = statsHtml;
}
// Mettre à jour les stats du bouton
const stepStats = document.getElementById(`stepStats${stepId}`);
if (stepStats && data.stats) {
const llmCount = data.result && data.result.llmCalls ? data.result.llmCalls.length : 0;
stepStats.innerHTML = `${data.stats.duration}ms<br>$${(data.stats.cost || 0).toFixed(3)}<br>${llmCount} LLM calls`;
}
}
function displayStepWarning(stepId, warningMessage) {
const contentDiv = document.getElementById(`resultContent${stepId}`);
const outputDiv = document.getElementById(`contentOutput${stepId}`);
contentDiv.style.display = 'block';
// Ajouter un warning en haut du contenu
const existingContent = outputDiv.innerHTML;
outputDiv.innerHTML = `<div style="background: #fed7aa; color: #c2410c; padding: 8px; border-radius: 4px; margin-bottom: 10px;">⚠️ ${warningMessage}</div>${existingContent}`;
}
function displayStepError(stepId, errorMessage) {
const contentDiv = document.getElementById(`resultContent${stepId}`);
const outputDiv = document.getElementById(`contentOutput${stepId}`);
contentDiv.style.display = 'block';
outputDiv.innerHTML = `<div style="color: #f56565;">❌ Erreur: ${errorMessage}</div>`;
const stepStats = document.getElementById(`stepStats${stepId}`);
if (stepStats) {
stepStats.textContent = 'Erreur';
}
}
function formatContentForTag(content) {
if (!content || typeof content !== 'object') {
return String(content || 'Pas de contenu');
}
return Object.entries(content)
.map(([tag, text]) => `[${tag}]\n${text}`)
.join('\n\n');
}
function formatContentForDisplay(content, format) {
if (format === 'tag') {
return content.replace(/\[([^\]]+)\]/g, '<span class="tag">[$1]</span>');
} else if (format === 'compact') {
// Mode compact pour before/after - texte plus petit et troncature
const truncated = content.length > 200 ? content.substring(0, 200) + '...' : content;
return truncated.replace(/\[([^\]]+)\]/g, '<span class="tag">[$1]</span>')
.replace(/\n/g, '<br>');
}
return content;
}
function toggleFormat(stepId, format) {
const buttons = document.querySelectorAll(`[data-step="${stepId}"] .format-btn`);
buttons.forEach(btn => btn.classList.remove('active'));
const activeBtn = document.querySelector(`[data-step="${stepId}"] .format-btn[onclick*="${format}"]`);
if (activeBtn) activeBtn.classList.add('active');
const data = stepResultsData[stepId];
if (!data) return;
const outputDiv = document.getElementById(`contentOutput${stepId}`);
if (format === 'tag' && data.result.formatted) {
outputDiv.innerHTML = formatContentForDisplay(data.result.formatted, 'tag');
} else if (format === 'xml' && data.result.xmlFormatted) {
outputDiv.textContent = data.result.xmlFormatted;
}
}
function enableControls() {
document.getElementById('executeAllBtn').disabled = false;
document.getElementById('resetBtn').disabled = false;
document.getElementById('exportBtn').disabled = false;
}
function updateGlobalStats() {
const results = Object.values(stepResultsData);
if (results.length === 0) {
// Reset stats
document.getElementById('totalDuration').textContent = '0ms';
document.getElementById('totalTokens').textContent = '0';
document.getElementById('totalCost').textContent = '$0.00';
document.getElementById('completedSteps').textContent = '0';
document.getElementById('successRate').textContent = '0%';
return;
}
const totalDuration = results.reduce((sum, r) => sum + (r.stats?.duration || 0), 0);
const totalTokens = results.reduce((sum, r) => sum + (r.stats?.tokensUsed || 0), 0);
const totalCost = results.reduce((sum, r) => sum + (r.stats?.cost || 0), 0);
const successfulResults = results.filter(r => r.success);
document.getElementById('totalDuration').textContent = `${totalDuration}ms`;
document.getElementById('totalTokens').textContent = totalTokens.toString();
document.getElementById('totalCost').textContent = `$${totalCost.toFixed(4)}`;
document.getElementById('completedSteps').textContent = results.length.toString();
document.getElementById('successRate').textContent = `${Math.round(successfulResults.length / results.length * 100)}%`;
document.getElementById('avgDuration').textContent = `${Math.round(totalDuration / results.length)}ms`;
document.getElementById('avgCost').textContent = `$${(totalCost / results.length).toFixed(4)}`;
document.getElementById('lastActivity').textContent = new Date().toLocaleTimeString();
// Trouver l'étape la plus lente
const slowestStep = results.reduce((max, r) =>
(r.stats?.duration || 0) > (max.stats?.duration || 0) ? r : max, results[0]);
if (slowestStep) {
document.getElementById('slowestStep').textContent = `Étape ${slowestStep.stepId}`;
}
}
// ==============================================
// EXPORT
// ==============================================
async function exportResults() {
if (!currentSessionId) {
alert('Aucune session à exporter');
return;
}
try {
const response = await fetch(`/api/step-by-step/export/${currentSessionId}`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `step-by-step-${currentSessionId}.json`;
a.click();
window.URL.revokeObjectURL(url);
console.log('💾 Export réalisé');
} catch (error) {
console.error('❌ Erreur export:', error);
alert('Erreur export: ' + error.message);
}
}
</script>
</body>
</html>