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

995 lines
33 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;
// 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 formaté
if (data.result && data.result.formatted) {
outputDiv.innerHTML = formatContentForDisplay(data.result.formatted, 'tag');
} else {
outputDiv.textContent = 'Pas de contenu généré';
}
// Afficher les stats
if (data.stats) {
statsDiv.innerHTML = `
<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>
`;
}
// Mettre à jour les stats du bouton
const stepStats = document.getElementById(`stepStats${stepId}`);
if (stepStats && data.stats) {
stepStats.innerHTML = `${data.stats.duration}ms<br>$${(data.stats.cost || 0).toFixed(3)}`;
}
}
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 formatContentForDisplay(content, format) {
if (format === 'tag') {
return content.replace(/\[([^\]]+)\]/g, '<span class="tag">[$1]</span>');
}
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>