1147 lines
42 KiB
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> |