|
<!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 BD</title> |
|
<style> |
|
* { |
|
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; |
|
} |
|
|
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
} |
|
|
|
.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; |
|
} |
|
|
|
.main-content { |
|
display: grid; |
|
grid-template-columns: 1fr 1fr; |
|
gap: 30px; |
|
margin-bottom: 30px; |
|
} |
|
|
|
.input-section, .status-section { |
|
background: rgba(255, 255, 255, 0.95); |
|
padding: 30px; |
|
border-radius: 15px; |
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
|
} |
|
|
|
.input-section h2, .status-section h2 { |
|
color: #4a5568; |
|
margin-bottom: 20px; |
|
border-bottom: 3px solid #667eea; |
|
padding-bottom: 10px; |
|
} |
|
|
|
.json-input { |
|
width: 100%; |
|
height: 400px; |
|
padding: 15px; |
|
border: 2px solid #e2e8f0; |
|
border-radius: 10px; |
|
font-family: 'Courier New', monospace; |
|
font-size: 14px; |
|
line-height: 1.5; |
|
resize: vertical; |
|
background: #f8fafc; |
|
} |
|
|
|
.json-input:focus { |
|
outline: none; |
|
border-color: #667eea; |
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); |
|
} |
|
|
|
.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; |
|
} |
|
|
|
.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; |
|
} |
|
|
|
.status-card { |
|
background: #f7fafc; |
|
border: 2px solid #e2e8f0; |
|
border-radius: 10px; |
|
padding: 20px; |
|
margin: 15px 0; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.status-card.generating { |
|
border-color: #f6ad55; |
|
background: #fffaf0; |
|
} |
|
|
|
.status-card.completed { |
|
border-color: #68d391; |
|
background: #f0fff4; |
|
} |
|
|
|
.status-card.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; |
|
} |
|
|
|
.download-btn { |
|
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); |
|
color: white; |
|
border: none; |
|
padding: 10px 20px; |
|
border-radius: 8px; |
|
cursor: pointer; |
|
font-weight: bold; |
|
text-decoration: none; |
|
display: inline-block; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.download-btn:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 5px 15px rgba(72, 187, 120, 0.3); |
|
} |
|
|
|
.example-section { |
|
background: rgba(255, 255, 255, 0.95); |
|
padding: 30px; |
|
border-radius: 15px; |
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
|
margin-top: 30px; |
|
} |
|
|
|
.example-json { |
|
background: #2d3748; |
|
color: #e2e8f0; |
|
padding: 20px; |
|
border-radius: 10px; |
|
font-family: 'Courier New', monospace; |
|
font-size: 14px; |
|
line-height: 1.5; |
|
overflow-x: auto; |
|
white-space: pre-wrap; |
|
} |
|
|
|
.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; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.main-content { |
|
grid-template-columns: 1fr; |
|
gap: 20px; |
|
} |
|
|
|
.header h1 { |
|
font-size: 2em; |
|
} |
|
|
|
.json-input { |
|
height: 300px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1>🎨 Générateur de Manga BD</h1> |
|
<p>Créez votre manga personnalisé avec l'IA Gemini</p> |
|
</div> |
|
|
|
<div class="main-content"> |
|
<div class="input-section"> |
|
<h2>📝 Configuration du Manga</h2> |
|
<textarea |
|
id="jsonInput" |
|
class="json-input" |
|
placeholder="Collez votre JSON de configuration ici..."> |
|
{ |
|
"partie-1": "Crée une page de manga style shonen avec un héros adolescent aux cheveux hérissés qui découvre ses pouvoirs magiques dans une forêt mystérieuse. Style artistique détaillé avec beaucoup d'effets visuels.", |
|
"partie-2": "Suite de l'histoire: le héros rencontre un mentor sage qui lui explique l'origine de ses pouvoirs. Scène dans une clairière avec des éléments magiques flottants.", |
|
"partie-3": "Combat épique contre un monstre des ombres. Le héros utilise ses nouveaux pouvoirs pour la première fois. Beaucoup d'action et d'effets spéciaux.", |
|
"partie-4": "Victoire du héros et résolution. Il regarde vers l'horizon, prêt pour de nouvelles aventures. Scène inspirante avec un coucher de soleil." |
|
}</textarea> |
|
<button id="generateBtn" class="btn"> |
|
🚀 Générer le Manga |
|
</button> |
|
</div> |
|
|
|
<div class="status-section"> |
|
<h2>📊 Statut de Génération</h2> |
|
<div id="statusContainer"> |
|
<p style="color: #718096; text-align: center; padding: 40px;"> |
|
Aucune génération en cours.<br> |
|
Collez votre configuration JSON et cliquez sur "Générer le Manga" pour commencer. |
|
</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="example-section"> |
|
<h2>📋 Format JSON Attendu</h2> |
|
<p style="margin-bottom: 20px;"> |
|
Votre JSON doit contenir des clés nommées "partie-X" (où X est un numéro) avec des prompts détaillés pour chaque page : |
|
</p> |
|
<div class="example-json">{ |
|
"partie-1": "Prompt détaillé pour la première page de votre manga...", |
|
"partie-2": "Prompt détaillé pour la deuxième page...", |
|
"partie-3": "Prompt détaillé pour la troisième page...", |
|
"partie-N": "Continuez avec autant de parties que nécessaire..." |
|
}</div> |
|
<p style="margin-top: 15px; color: #4a5568;"> |
|
<strong>Conseils :</strong> Soyez très descriptif dans vos prompts. Mentionnez le style artistique, |
|
les personnages, l'action, l'ambiance, etc. Plus votre description est détaillée, |
|
meilleur sera le résultat ! |
|
</p> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
let currentTaskId = null; |
|
let statusInterval = null; |
|
|
|
const generateBtn = document.getElementById('generateBtn'); |
|
const jsonInput = document.getElementById('jsonInput'); |
|
const statusContainer = document.getElementById('statusContainer'); |
|
|
|
generateBtn.addEventListener('click', async () => { |
|
const jsonText = jsonInput.value.trim(); |
|
|
|
if (!jsonText) { |
|
showAlert('Veuillez saisir une configuration JSON', 'error'); |
|
return; |
|
} |
|
|
|
let jsonData; |
|
try { |
|
jsonData = JSON.parse(jsonText); |
|
} catch (error) { |
|
showAlert('Format JSON invalide: ' + error.message, 'error'); |
|
return; |
|
} |
|
|
|
|
|
const parts = Object.keys(jsonData).filter(key => key.startsWith('partie-')); |
|
if (parts.length === 0) { |
|
showAlert('Aucune partie trouvée dans le JSON. Utilisez des clés comme "partie-1", "partie-2", etc.', 'error'); |
|
return; |
|
} |
|
|
|
try { |
|
generateBtn.disabled = true; |
|
generateBtn.innerHTML = '<span class="spinner"></span> 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) { |
|
currentTaskId = result.task_id; |
|
startStatusPolling(); |
|
showAlert(`Génération démarrée ! ID de tâche: ${currentTaskId}`, 'success'); |
|
} else { |
|
throw new Error(result.error || 'Erreur inconnue'); |
|
} |
|
|
|
} catch (error) { |
|
showAlert('Erreur lors du démarrage: ' + error.message, 'error'); |
|
generateBtn.disabled = false; |
|
generateBtn.innerHTML = '🚀 Générer le Manga'; |
|
} |
|
}); |
|
|
|
function startStatusPolling() { |
|
if (statusInterval) { |
|
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 (status.status === 'completed' || status.status === 'error') { |
|
clearInterval(statusInterval); |
|
statusInterval = null; |
|
generateBtn.disabled = false; |
|
generateBtn.innerHTML = '🚀 Générer le Manga'; |
|
} |
|
} else { |
|
console.error('Erreur lors de la récupération du statut:', status.error); |
|
} |
|
} catch (error) { |
|
console.error('Erreur réseau:', error); |
|
} |
|
}, 2000); |
|
} |
|
|
|
function updateStatusDisplay(status) { |
|
const container = statusContainer; |
|
|
|
let statusClass = ''; |
|
let statusIcon = ''; |
|
let statusText = ''; |
|
|
|
switch (status.status) { |
|
case 'queued': |
|
statusClass = 'generating'; |
|
statusIcon = '⏳'; |
|
statusText = 'En file d\'attente'; |
|
break; |
|
case 'generating': |
|
statusClass = 'generating'; |
|
statusIcon = '🎨'; |
|
statusText = 'Génération en cours'; |
|
break; |
|
case 'creating_pdf': |
|
statusClass = 'generating'; |
|
statusIcon = '📄'; |
|
statusText = 'Création du PDF'; |
|
break; |
|
case 'completed': |
|
statusClass = 'completed'; |
|
statusIcon = '✅'; |
|
statusText = 'Terminé !'; |
|
break; |
|
case 'error': |
|
statusClass = 'error'; |
|
statusIcon = '❌'; |
|
statusText = 'Erreur'; |
|
break; |
|
} |
|
|
|
let progressHtml = ''; |
|
if (status.total_pages && status.current_page) { |
|
const progress = (status.current_page / status.total_pages) * 100; |
|
progressHtml = ` |
|
<div class="progress-bar"> |
|
<div class="progress-fill" style="width: ${progress}%"></div> |
|
</div> |
|
<p style="text-align: center; margin-top: 10px;"> |
|
Page ${status.current_page} sur ${status.total_pages} |
|
${status.current_part ? `(${status.current_part})` : ''} |
|
</p> |
|
`; |
|
} |
|
|
|
let downloadHtml = ''; |
|
if (status.status === 'completed') { |
|
downloadHtml = ` |
|
<a href="/download/${currentTaskId}" class="download-btn" style="width: 100%; text-align: center; margin-top: 15px;"> |
|
📥 Télécharger le PDF |
|
</a> |
|
`; |
|
} |
|
|
|
let errorHtml = ''; |
|
if (status.error) { |
|
errorHtml = `<div class="alert alert-error">${status.error}</div>`; |
|
} |
|
|
|
container.innerHTML = ` |
|
<div class="status-card ${statusClass}"> |
|
<h3>${statusIcon} ${statusText}</h3> |
|
<p><strong>ID de tâche:</strong> ${currentTaskId}</p> |
|
<p><strong>Créée le:</strong> ${new Date(status.created_at).toLocaleString('fr-FR')}</p> |
|
${status.completed_at ? `<p><strong>Terminée le:</strong> ${new Date(status.completed_at).toLocaleString('fr-FR')}</p>` : ''} |
|
${progressHtml} |
|
${errorHtml} |
|
${downloadHtml} |
|
</div> |
|
`; |
|
} |
|
|
|
function showAlert(message, type) { |
|
const alertClass = type === 'error' ? 'alert-error' : 'alert-success'; |
|
const alertHtml = `<div class="alert ${alertClass}">${message}</div>`; |
|
|
|
statusContainer.innerHTML = alertHtml + statusContainer.innerHTML; |
|
|
|
|
|
setTimeout(() => { |
|
const alert = statusContainer.querySelector('.alert'); |
|
if (alert) { |
|
alert.remove(); |
|
} |
|
}, 5000); |
|
} |
|
|
|
|
|
window.addEventListener('beforeunload', () => { |
|
if (statusInterval) { |
|
clearInterval(statusInterval); |
|
} |
|
}); |
|
|
|
|
|
jsonInput.addEventListener('blur', () => { |
|
try { |
|
const parsed = JSON.parse(jsonInput.value); |
|
jsonInput.value = JSON.stringify(parsed, null, 2); |
|
} catch (error) { |
|
|
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |