Eps2 / templates /index.html
Docfile's picture
Update templates/index.html
c825bc1 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Générateur de Manga IA</title>
<style>
/* --- Styles Généraux --- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* --- En-tête --- */
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
color: white;
font-size: 3em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
color: rgba(255,255,255,0.9);
font-size: 1.2em;
}
/* --- Grille principale --- */
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
/* --- Cartes de section --- */
.card {
background: rgba(255, 255, 255, 0.98);
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.card h2 {
color: #4a5568;
margin-bottom: 20px;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
/* --- Champs de saisie --- */
.json-input {
width: 100%;
height: 400px;
padding: 15px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
background: #f8fafc;
}
.json-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* --- Boutons --- */
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s ease;
width: 100%;
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.btn:disabled {
background: #a0aec0;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.download-btn {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
text-decoration: none;
display: inline-block;
margin-top: 15px;
padding: 12px 24px;
}
.download-btn:hover {
box-shadow: 0 5px 15px rgba(72, 187, 120, 0.3);
}
/* --- Section Statut --- */
.status-card-display {
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 20px;
margin: 15px 0;
transition: all 0.3s ease;
}
.status-card-display.generating { border-color: #f6ad55; background: #fffaf0; }
.status-card-display.completed { border-color: #68d391; background: #f0fff4; }
.status-card-display.error { border-color: #fc8181; background: #fffafa; }
.progress-bar {
background: #e2e8f0;
border-radius: 10px;
height: 20px;
margin: 15px 0;
overflow: hidden;
}
.progress-fill {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100%;
transition: width 0.5s ease;
border-radius: 10px;
}
/* --- Section Prévisualisation --- */
.preview-section {
display: none; /* Caché par défaut */
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.image-grid img {
width: 100%;
height: auto;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
object-fit: cover;
aspect-ratio: 2 / 3;
background-color: #f0f0f0; /* Placeholder color */
}
/* --- Alertes et Spinner --- */
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.alert {
padding: 15px;
border-radius: 10px;
margin: 15px 0;
font-weight: bold;
}
.alert-error { background: #fed7d7; color: #c53030; border: 2px solid #fc8181; }
.alert-success { background: #c6f6d5; color: #2f855a; border: 2px solid #68d391; }
/* --- Section d'Exemple JSON --- */
.example-json {
background: #2d3748;
color: #e2e8f0;
padding: 20px;
border-radius: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
overflow-x: auto;
white-space: pre-wrap;
}
/* --- Responsive Design --- */
@media (max-width: 900px) {
.main-content { grid-template-columns: 1fr; }
.header h1 { font-size: 2.5em; }
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>🎨 Générateur de Manga IA</h1>
<p>Créez votre manga personnalisé avec l'IA Gemini. Décrivez chaque page et laissez la magie opérer !</p>
</header>
<main class="main-content">
<section id="input-section" class="card">
<h2>📝 Scénario du Manga</h2>
<textarea id="jsonInput" class="json-input" placeholder="Collez votre JSON de configuration ici...">{
"partie-1": "Page de titre, style manga épique. Un jeune épéiste aux cheveux argentés se tient sur une falaise, regardant une ville futuriste sous une lune brisée. Titre : 'CHRONIQUES DE NÉO-KYOTO'.",
"partie-2": "Style manga noir et blanc. L'épéiste, Kai, marche dans une ruelle sombre de Néo-Kyoto, éclairée par des néons. Il a l'air méfiant.",
"partie-3": "Action intense. Kai esquive une attaque laser d'un drone robotique. Traits de vitesse et effets d'impact. Style dynamique.",
"partie-4": "Gros plan sur le visage déterminé de Kai alors qu'il active son épée énergétique, qui brille d'une lumière bleue. Il se prépare à contre-attaquer."
}</textarea>
<button id="generateBtn" class="btn">
<span id="btnIcon">🚀</span>
<span id="btnText">Générer le Manga</span>
</button>
</section>
<section id="status-section" class="card">
<h2>📊 Suivi de Génération</h2>
<div id="statusContainer">
<p style="color: #718096; text-align: center; padding: 40px;">
Prêt à démarrer.<br>
Décrivez votre scénario et cliquez sur "Générer" pour commencer.
</p>
</div>
</section>
</main>
<section id="previewSection" class="card preview-section">
<h2>🖼️ Prévisualisation des Pages</h2>
<div id="imageGrid" class="image-grid">
<!-- Les images générées apparaîtront ici dynamiquement -->
</div>
</section>
<section class="card">
<h2>📋 Format du Scénario (JSON)</h2>
<p style="margin-bottom: 20px;">
Utilisez des clés "partie-X" (où X est un numéro) avec une description détaillée pour chaque page :
</p>
<div class="example-json">{
"partie-1": "Description détaillée de la première page...",
"partie-2": "Description détaillée de la deuxième page...",
"partie-N": "Continuez avec autant de parties que nécessaire..."
}</div>
<p style="margin-top: 15px; color: #4a5568;">
<strong>Conseils :</strong> Soyez précis ! Mentionnez le style (noir et blanc, couleur, shonen, etc.), les personnages, l'action, l'ambiance et l'angle de caméra pour de meilleurs résultats.
</p>
</section>
</div>
<script>
// --- Variables globales ---
let currentTaskId = null;
let statusInterval = null;
// --- Éléments du DOM ---
const generateBtn = document.getElementById('generateBtn');
const jsonInput = document.getElementById('jsonInput');
const statusContainer = document.getElementById('statusContainer');
const previewSection = document.getElementById('previewSection');
const imageGrid = document.getElementById('imageGrid');
const btnIcon = document.getElementById('btnIcon');
const btnText = document.getElementById('btnText');
// --- Écouteurs d'événements ---
generateBtn.addEventListener('click', startGeneration);
jsonInput.addEventListener('blur', autoFormatJSON);
window.addEventListener('beforeunload', () => clearInterval(statusInterval));
// --- Fonctions principales ---
async function startGeneration() {
// 1. Réinitialiser l'interface
resetUI();
// 2. Valider le JSON
let jsonData;
try {
const jsonText = jsonInput.value.trim();
if (!jsonText) throw new Error("Veuillez saisir un scénario JSON.");
jsonData = JSON.parse(jsonText);
if (Object.keys(jsonData).filter(k => k.startsWith('partie-')).length === 0) {
throw new Error('Aucune "partie-X" trouvée dans le JSON.');
}
} catch (error) {
showAlert(`Erreur de format JSON : ${error.message}`, 'error');
return;
}
// 3. Envoyer la requête de génération
try {
setButtonState(true, 'Démarrage...');
const response = await fetch('/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonData)
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Erreur inconnue du serveur.');
// 4. Démarrer le suivi de la progression
currentTaskId = result.task_id;
startStatusPolling();
showAlert(`Génération démarrée ! Tâche ID : ${currentTaskId}`, 'success');
} catch (error) {
showAlert(`Erreur de démarrage : ${error.message}`, 'error');
setButtonState(false);
}
}
function startStatusPolling() {
clearInterval(statusInterval);
statusInterval = setInterval(async () => {
if (!currentTaskId) return;
try {
const response = await fetch(`/status/${currentTaskId}`);
const status = await response.json();
if (response.ok) {
updateStatusDisplay(status);
if (['completed', 'error'].includes(status.status)) {
clearInterval(statusInterval);
setButtonState(false);
}
} else {
console.error('Erreur de statut:', status.error);
}
} catch (error) {
console.error('Erreur réseau:', error);
}
}, 3000); // Vérifier toutes les 3 secondes
}
function updateStatusDisplay(status) {
// 1. Mettre à jour la carte de statut
const statusInfo = getStatusInfo(status.status);
const progressHtml = getProgressHtml(status);
const downloadHtml = status.status === 'completed' ? getDownloadButtonHtml() : '';
const errorHtml = status.error ? `<div class="alert alert-error">${status.error}</div>` : '';
statusContainer.innerHTML = `
<div class="status-card-display ${statusInfo.class}">
<h3>${statusInfo.icon} ${statusInfo.text}</h3>
<p><small><strong>ID :</strong> ${currentTaskId}</small></p>
${progressHtml}
${errorHtml}
${downloadHtml}
</div>`;
// 2. Mettre à jour la grille d'images
if (status.image_urls && status.image_urls.length > 0) {
previewSection.style.display = 'block';
status.image_urls.forEach(url => {
if (!document.querySelector(`img[src="${url}"]`)) {
const img = document.createElement('img');
img.src = url;
img.alt = `Page générée pour la tâche ${currentTaskId}`;
img.style.opacity = 0;
img.onload = () => {
img.style.transition = 'opacity 0.5s ease-in-out';
img.style.opacity = 1;
};
imageGrid.appendChild(img);
}
});
}
}
// --- Fonctions utilitaires ---
function resetUI() {
imageGrid.innerHTML = '';
previewSection.style.display = 'none';
if(statusContainer.querySelector('.alert')) {
statusContainer.querySelector('.alert').remove();
}
}
function setButtonState(isLoading, text = 'Générer le Manga') {
generateBtn.disabled = isLoading;
btnText.textContent = text;
btnIcon.innerHTML = isLoading ? '<span class="spinner"></span>' : '🚀';
}
function getStatusInfo(status) {
const map = {
'queued': { icon: '⏳', text: 'En file d\'attente', class: 'generating' },
'generating': { icon: '🎨', text: 'Génération en cours...', class: 'generating' },
'creating_zip': { icon: '🗜️', text: 'Création de l\'archive ZIP...', class: 'generating' },
'completed': { icon: '✅', text: 'Terminé !', class: 'completed' },
'error': { icon: '❌', text: 'Erreur', class: 'error' }
};
return map[status] || { icon: '❔', text: 'Inconnu', class: '' };
}
function getProgressHtml(status) {
if (!status.total_pages || status.status === 'completed') return '';
const progress = (status.current_page / status.total_pages) * 100;
return `
<p style="text-align: center; margin-top: 10px;">
Page ${status.current_page || 0} sur ${status.total_pages}
</p>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>`;
}
function getDownloadButtonHtml() {
return `<a href="/download/${currentTaskId}" class="btn download-btn">📥 Télécharger l'archive ZIP</a>`;
}
function showAlert(message, type) {
const alertClass = type === 'error' ? 'alert-error' : 'alert-success';
const alertHtml = `<div class="alert ${alertClass}">${message}</div>`;
statusContainer.innerHTML = alertHtml;
}
function autoFormatJSON() {
try {
const parsed = JSON.parse(jsonInput.value);
jsonInput.value = JSON.stringify(parsed, null, 2); // 2 espaces pour l'indentation
} catch (e) {
// Si le JSON est invalide, ne rien faire pour ne pas effacer la saisie de l'utilisateur
}
}
</script>
</body>
</html>