Docfile commited on
Commit
c825bc1
·
verified ·
1 Parent(s): 74a519a

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +201 -312
templates/index.html CHANGED
@@ -3,45 +3,44 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Générateur de Manga BD</title>
7
  <style>
8
- /* ... (TOUT VOTRE CSS RESTE IDENTIQUE) ... */
9
  * {
10
  margin: 0;
11
  padding: 0;
12
  box-sizing: border-box;
13
  }
14
-
15
  body {
16
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
  min-height: 100vh;
19
  color: #333;
 
20
  }
21
-
22
  .container {
23
  max-width: 1200px;
24
  margin: 0 auto;
25
  padding: 20px;
26
  }
27
 
 
28
  .header {
29
  text-align: center;
30
  margin-bottom: 40px;
31
  }
32
-
33
  .header h1 {
34
  color: white;
35
  font-size: 3em;
36
  margin-bottom: 10px;
37
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
38
  }
39
-
40
  .header p {
41
  color: rgba(255,255,255,0.9);
42
  font-size: 1.2em;
43
  }
44
 
 
45
  .main-content {
46
  display: grid;
47
  grid-template-columns: 1fr 1fr;
@@ -49,20 +48,21 @@
49
  margin-bottom: 30px;
50
  }
51
 
52
- .input-section, .status-section {
53
- background: rgba(255, 255, 255, 0.95);
 
54
  padding: 30px;
55
  border-radius: 15px;
56
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
57
  }
58
-
59
- .input-section h2, .status-section h2 {
60
  color: #4a5568;
61
  margin-bottom: 20px;
62
  border-bottom: 3px solid #667eea;
63
  padding-bottom: 10px;
64
  }
65
 
 
66
  .json-input {
67
  width: 100%;
68
  height: 400px;
@@ -71,17 +71,16 @@
71
  border-radius: 10px;
72
  font-family: 'Courier New', monospace;
73
  font-size: 14px;
74
- line-height: 1.5;
75
  resize: vertical;
76
  background: #f8fafc;
77
  }
78
-
79
  .json-input:focus {
80
  outline: none;
81
  border-color: #667eea;
82
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
83
  }
84
 
 
85
  .btn {
86
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
87
  color: white;
@@ -94,21 +93,35 @@
94
  transition: all 0.3s ease;
95
  width: 100%;
96
  margin-top: 20px;
 
 
 
 
97
  }
98
-
99
  .btn:hover {
100
  transform: translateY(-2px);
101
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
102
  }
103
-
104
  .btn:disabled {
105
  background: #a0aec0;
106
  cursor: not-allowed;
107
  transform: none;
108
  box-shadow: none;
109
  }
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- .status-card {
 
112
  background: #f7fafc;
113
  border: 2px solid #e2e8f0;
114
  border-radius: 10px;
@@ -116,21 +129,9 @@
116
  margin: 15px 0;
117
  transition: all 0.3s ease;
118
  }
119
-
120
- .status-card.generating {
121
- border-color: #f6ad55;
122
- background: #fffaf0;
123
- }
124
-
125
- .status-card.completed {
126
- border-color: #68d391;
127
- background: #f0fff4;
128
- }
129
-
130
- .status-card.error {
131
- border-color: #fc8181;
132
- background: #fffafa;
133
- }
134
 
135
  .progress-bar {
136
  background: #e2e8f0;
@@ -139,7 +140,6 @@
139
  margin: 15px 0;
140
  overflow: hidden;
141
  }
142
-
143
  .progress-fill {
144
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
145
  height: 100%;
@@ -147,44 +147,27 @@
147
  border-radius: 10px;
148
  }
149
 
150
- .download-btn {
151
- background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
152
- color: white;
153
- border: none;
154
- padding: 10px 20px;
155
- border-radius: 8px;
156
- cursor: pointer;
157
- font-weight: bold;
158
- text-decoration: none;
159
- display: inline-block;
160
- transition: all 0.3s ease;
161
- }
162
-
163
- .download-btn:hover {
164
- transform: translateY(-2px);
165
- box-shadow: 0 5px 15px rgba(72, 187, 120, 0.3);
166
  }
167
-
168
- .example-section {
169
- background: rgba(255, 255, 255, 0.95);
170
- padding: 30px;
171
- border-radius: 15px;
172
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
173
- margin-top: 30px;
174
  }
175
-
176
- .example-json {
177
- background: #2d3748;
178
- color: #e2e8f0;
179
- padding: 20px;
180
  border-radius: 10px;
181
- font-family: 'Courier New', monospace;
182
- font-size: 14px;
183
- line-height: 1.5;
184
- overflow-x: auto;
185
- white-space: pre-wrap;
186
  }
187
 
 
188
  .spinner {
189
  display: inline-block;
190
  width: 20px;
@@ -194,361 +177,267 @@
194
  border-top-color: #fff;
195
  animation: spin 1s ease-in-out infinite;
196
  }
197
-
198
- @keyframes spin {
199
- to { transform: rotate(360deg); }
200
- }
201
-
202
  .alert {
203
  padding: 15px;
204
  border-radius: 10px;
205
  margin: 15px 0;
206
  font-weight: bold;
207
  }
 
 
208
 
209
- .alert-error {
210
- background: #fed7d7;
211
- color: #c53030;
212
- border: 2px solid #fc8181;
213
- }
214
-
215
- .alert-success {
216
- background: #c6f6d5;
217
- color: #2f855a;
218
- border: 2px solid #68d391;
219
- }
220
-
221
- /* NOUVEAUX STYLES POUR LA GRILLE D'IMAGES */
222
- .preview-section {
223
- background: rgba(255, 255, 255, 0.95);
224
- padding: 30px;
225
- border-radius: 15px;
226
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
227
- margin-top: 30px;
228
- display: none; /* Caché par défaut */
229
- }
230
-
231
- .image-grid {
232
- display: grid;
233
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
234
- gap: 20px;
235
- }
236
-
237
- .image-grid img {
238
- width: 100%;
239
- height: auto;
240
  border-radius: 10px;
241
- box-shadow: 0 5px 15px rgba(0,0,0,0.1);
242
- object-fit: cover;
243
- aspect-ratio: 2 / 3; /* Ratio commun pour les pages de manga */
 
244
  }
245
-
246
- @media (max-width: 768px) {
247
- .main-content {
248
- grid-template-columns: 1fr;
249
- gap: 20px;
250
- }
251
 
252
- .header h1 {
253
- font-size: 2em;
254
- }
255
-
256
- .json-input {
257
- height: 300px;
258
- }
259
  }
260
  </style>
261
  </head>
262
  <body>
263
  <div class="container">
264
- <div class="header">
265
- <h1>🎨 Générateur de Manga BD</h1>
266
- <p>Créez votre manga personnalisé avec l'IA Gemini</p>
267
- </div>
268
-
269
- <div class="main-content">
270
- <!-- ... (La section input reste la même) ... -->
271
- <div class="input-section">
272
- <h2>📝 Configuration du Manga</h2>
273
- <textarea
274
- id="jsonInput"
275
- class="json-input"
276
- placeholder="Collez votre JSON de configuration ici...">
277
- {
278
- "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.",
279
- "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.",
280
- "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.",
281
- "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."
282
  }</textarea>
283
  <button id="generateBtn" class="btn">
284
- 🚀 Générer le Manga
 
285
  </button>
286
- </div>
287
- <div class="status-section">
288
- <h2>📊 Statut de Génération</h2>
 
289
  <div id="statusContainer">
290
  <p style="color: #718096; text-align: center; padding: 40px;">
291
- Aucune génération en cours.<br>
292
- Collez votre configuration JSON et cliquez sur "Générer le Manga" pour commencer.
293
  </p>
294
  </div>
295
- </div>
296
- </div>
297
-
298
- <!-- NOUVELLE SECTION POUR L'AFFICHAGE DES IMAGES -->
299
- <div id="previewSection" class="preview-section">
300
  <h2>🖼️ Prévisualisation des Pages</h2>
301
  <div id="imageGrid" class="image-grid">
302
- <!-- Les images générées apparaîtront ici -->
303
  </div>
304
- </div>
305
 
306
- <!-- ... (La section exemple reste la même) ... -->
307
- <div class="example-section">
308
- <h2>📋 Format JSON Attendu</h2>
309
  <p style="margin-bottom: 20px;">
310
- 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 :
311
  </p>
312
  <div class="example-json">{
313
- "partie-1": "Prompt détaillé pour la première page de votre manga...",
314
- "partie-2": "Prompt détaillé pour la deuxième page...",
315
- "partie-3": "Prompt détaillé pour la troisième page...",
316
  "partie-N": "Continuez avec autant de parties que nécessaire..."
317
  }</div>
318
  <p style="margin-top: 15px; color: #4a5568;">
319
- <strong>Conseils :</strong> Soyez très descriptif dans vos prompts. Mentionnez le style artistique,
320
- les personnages, l'action, l'ambiance, etc. Plus votre description est détaillée,
321
- meilleur sera le résultat !
322
  </p>
323
- </div>
 
324
  </div>
325
 
326
  <script>
327
- // ... (Les variables globales restent les mêmes) ...
328
  let currentTaskId = null;
329
  let statusInterval = null;
330
 
 
331
  const generateBtn = document.getElementById('generateBtn');
332
  const jsonInput = document.getElementById('jsonInput');
333
  const statusContainer = document.getElementById('statusContainer');
334
  const previewSection = document.getElementById('previewSection');
335
  const imageGrid = document.getElementById('imageGrid');
 
 
 
 
 
 
 
336
 
337
- generateBtn.addEventListener('click', async () => {
338
- // ... (Cette fonction reste quasi identique, on ajoute juste un reset de l'affichage)
339
-
340
- // Reset de l'interface
341
- imageGrid.innerHTML = '';
342
- previewSection.style.display = 'none';
343
 
344
- const jsonText = jsonInput.value.trim();
345
- if (!jsonText) {
346
- showAlert('Veuillez saisir une configuration JSON', 'error');
347
- return;
348
- }
349
- // ... (Le reste du code de la fonction est inchangé)
350
  let jsonData;
351
  try {
 
 
352
  jsonData = JSON.parse(jsonText);
 
 
 
353
  } catch (error) {
354
- showAlert('Format JSON invalide: ' + error.message, 'error');
355
- return;
356
- }
357
-
358
- // Vérifier que le JSON contient des parties
359
- const parts = Object.keys(jsonData).filter(key => key.startsWith('partie-'));
360
- if (parts.length === 0) {
361
- showAlert('Aucune partie trouvée dans le JSON. Utilisez des clés comme "partie-1", "partie-2", etc.', 'error');
362
  return;
363
  }
364
-
 
365
  try {
366
- generateBtn.disabled = true;
367
- generateBtn.innerHTML = '<span class="spinner"></span> Démarrage...';
368
-
369
  const response = await fetch('/generate', {
370
  method: 'POST',
371
- headers: {
372
- 'Content-Type': 'application/json',
373
- },
374
  body: JSON.stringify(jsonData)
375
  });
376
-
377
  const result = await response.json();
378
 
379
- if (response.ok) {
380
- currentTaskId = result.task_id;
381
- startStatusPolling();
382
- showAlert(`Génération démarrée ! ID de tâche: ${currentTaskId}`, 'success');
383
- } else {
384
- throw new Error(result.error || 'Erreur inconnue');
385
- }
386
 
387
  } catch (error) {
388
- showAlert('Erreur lors du démarrage: ' + error.message, 'error');
389
- generateBtn.disabled = false;
390
- generateBtn.innerHTML = '🚀 Générer le Manga';
391
  }
392
- });
393
 
394
- // La fonction startStatusPolling reste la même
395
  function startStatusPolling() {
396
- if (statusInterval) {
397
- clearInterval(statusInterval);
398
- }
399
-
400
  statusInterval = setInterval(async () => {
401
  if (!currentTaskId) return;
402
-
403
  try {
404
  const response = await fetch(`/status/${currentTaskId}`);
405
  const status = await response.json();
406
-
407
  if (response.ok) {
408
  updateStatusDisplay(status);
409
-
410
- if (status.status === 'completed' || status.status === 'error') {
411
  clearInterval(statusInterval);
412
- statusInterval = null;
413
- generateBtn.disabled = false;
414
- generateBtn.innerHTML = '🚀 Générer le Manga';
415
  }
416
  } else {
417
- console.error('Erreur lors de la récupération du statut:', status.error);
418
  }
419
  } catch (error) {
420
  console.error('Erreur réseau:', error);
421
  }
422
- }, 2000); // Vérifier toutes les 2 secondes
423
  }
424
 
425
- // MISE À JOUR MAJEURE DE CETTE FONCTION
426
  function updateStatusDisplay(status) {
427
- const container = statusContainer;
428
-
429
- let statusClass = '';
430
- let statusIcon = '';
431
- let statusText = '';
432
-
433
- switch (status.status) {
434
- case 'queued':
435
- statusClass = 'generating';
436
- statusIcon = '⏳';
437
- statusText = 'En file d\'attente';
438
- break;
439
- case 'generating':
440
- statusClass = 'generating';
441
- statusIcon = '🎨';
442
- statusText = 'Génération en cours';
443
- break;
444
- case 'creating_zip': // STATUT MIS À JOUR
445
- statusClass = 'generating';
446
- statusIcon = '🗜️'; // Icône de compression
447
- statusText = 'Création de l\'archive ZIP';
448
- break;
449
- case 'completed':
450
- statusClass = 'completed';
451
- statusIcon = '✅';
452
- statusText = 'Terminé !';
453
- break;
454
- case 'error':
455
- statusClass = 'error';
456
- statusIcon = '❌';
457
- statusText = 'Erreur';
458
- break;
459
- }
460
-
461
- // ... (La logique de la barre de progression reste la même)
462
- let progressHtml = '';
463
- if (status.total_pages && status.current_page) {
464
- const progress = (status.current_page / status.total_pages) * 100;
465
- progressHtml = `
466
- <div class="progress-bar">
467
- <div class="progress-fill" style="width: ${progress}%"></div>
468
- </div>
469
- <p style="text-align: center; margin-top: 10px;">
470
- Page ${status.current_page} sur ${status.total_pages}
471
- ${status.current_part ? `(${status.current_part})` : ''}
472
- </p>
473
- `;
474
- }
475
-
476
- let downloadHtml = '';
477
- if (status.status === 'completed') {
478
- downloadHtml = `
479
- <a href="/download/${currentTaskId}" class="download-btn" style="width: 100%; text-align: center; margin-top: 15px;">
480
- 📥 Télécharger l'archive ZIP
481
- </a>
482
- `;
483
- }
484
-
485
- // ... (Le HTML pour l'erreur reste le même)
486
- let errorHtml = '';
487
- if (status.error) {
488
- errorHtml = `<div class="alert alert-error">${status.error}</div>`;
489
- }
490
-
491
- // Affichage du statut
492
- container.innerHTML = `
493
- <div class="status-card ${statusClass}">
494
- <h3>${statusIcon} ${statusText}</h3>
495
- <p><strong>ID de tâche:</strong> ${currentTaskId}</p>
496
- <p><strong>Créée le:</strong> ${new Date(status.created_at).toLocaleString('fr-FR')}</p>
497
- ${status.completed_at ? `<p><strong>Terminée le:</strong> ${new Date(status.completed_at).toLocaleString('fr-FR')}</p>` : ''}
498
  ${progressHtml}
499
  ${errorHtml}
500
  ${downloadHtml}
501
- </div>
502
- `;
503
 
504
- // NOUVELLE PARTIE : Affichage des images
505
  if (status.image_urls && status.image_urls.length > 0) {
506
  previewSection.style.display = 'block';
507
-
508
  status.image_urls.forEach(url => {
509
- // Vérifier si l'image n'est pas déjà affichée
510
  if (!document.querySelector(`img[src="${url}"]`)) {
511
  const img = document.createElement('img');
512
  img.src = url;
513
  img.alt = `Page générée pour la tâche ${currentTaskId}`;
514
- img.onload = () => img.style.opacity = 1; // Effet d'apparition
515
  img.style.opacity = 0;
516
- img.style.transition = 'opacity 0.5s';
 
 
 
517
  imageGrid.appendChild(img);
518
  }
519
  });
520
  }
521
  }
522
 
523
- // ... (Le reste du script JS reste identique)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  function showAlert(message, type) {
525
  const alertClass = type === 'error' ? 'alert-error' : 'alert-success';
526
  const alertHtml = `<div class="alert ${alertClass}">${message}</div>`;
527
-
528
- statusContainer.innerHTML = alertHtml + statusContainer.innerHTML;
529
-
530
- setTimeout(() => {
531
- const alert = statusContainer.querySelector('.alert');
532
- if (alert) {
533
- alert.remove();
534
- }
535
- }, 5000);
536
  }
537
 
538
- window.addEventListener('beforeunload', () => {
539
- if (statusInterval) {
540
- clearInterval(statusInterval);
541
- }
542
- });
543
-
544
- jsonInput.addEventListener('blur', () => {
545
  try {
546
  const parsed = JSON.parse(jsonInput.value);
547
- jsonInput.value = JSON.stringify(parsed, null, 2);
548
- } catch (error) {
549
- // Ignorer les erreurs
550
  }
551
- });
552
  </script>
553
  </body>
554
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Générateur de Manga IA</title>
7
  <style>
8
+ /* --- Styles Généraux --- */
9
  * {
10
  margin: 0;
11
  padding: 0;
12
  box-sizing: border-box;
13
  }
 
14
  body {
15
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
  min-height: 100vh;
18
  color: #333;
19
+ line-height: 1.6;
20
  }
 
21
  .container {
22
  max-width: 1200px;
23
  margin: 0 auto;
24
  padding: 20px;
25
  }
26
 
27
+ /* --- En-tête --- */
28
  .header {
29
  text-align: center;
30
  margin-bottom: 40px;
31
  }
 
32
  .header h1 {
33
  color: white;
34
  font-size: 3em;
35
  margin-bottom: 10px;
36
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
37
  }
 
38
  .header p {
39
  color: rgba(255,255,255,0.9);
40
  font-size: 1.2em;
41
  }
42
 
43
+ /* --- Grille principale --- */
44
  .main-content {
45
  display: grid;
46
  grid-template-columns: 1fr 1fr;
 
48
  margin-bottom: 30px;
49
  }
50
 
51
+ /* --- Cartes de section --- */
52
+ .card {
53
+ background: rgba(255, 255, 255, 0.98);
54
  padding: 30px;
55
  border-radius: 15px;
56
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
57
  }
58
+ .card h2 {
 
59
  color: #4a5568;
60
  margin-bottom: 20px;
61
  border-bottom: 3px solid #667eea;
62
  padding-bottom: 10px;
63
  }
64
 
65
+ /* --- Champs de saisie --- */
66
  .json-input {
67
  width: 100%;
68
  height: 400px;
 
71
  border-radius: 10px;
72
  font-family: 'Courier New', monospace;
73
  font-size: 14px;
 
74
  resize: vertical;
75
  background: #f8fafc;
76
  }
 
77
  .json-input:focus {
78
  outline: none;
79
  border-color: #667eea;
80
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
81
  }
82
 
83
+ /* --- Boutons --- */
84
  .btn {
85
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
86
  color: white;
 
93
  transition: all 0.3s ease;
94
  width: 100%;
95
  margin-top: 20px;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ gap: 10px;
100
  }
 
101
  .btn:hover {
102
  transform: translateY(-2px);
103
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
104
  }
 
105
  .btn:disabled {
106
  background: #a0aec0;
107
  cursor: not-allowed;
108
  transform: none;
109
  box-shadow: none;
110
  }
111
+ .download-btn {
112
+ background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
113
+ color: white;
114
+ text-decoration: none;
115
+ display: inline-block;
116
+ margin-top: 15px;
117
+ padding: 12px 24px;
118
+ }
119
+ .download-btn:hover {
120
+ box-shadow: 0 5px 15px rgba(72, 187, 120, 0.3);
121
+ }
122
 
123
+ /* --- Section Statut --- */
124
+ .status-card-display {
125
  background: #f7fafc;
126
  border: 2px solid #e2e8f0;
127
  border-radius: 10px;
 
129
  margin: 15px 0;
130
  transition: all 0.3s ease;
131
  }
132
+ .status-card-display.generating { border-color: #f6ad55; background: #fffaf0; }
133
+ .status-card-display.completed { border-color: #68d391; background: #f0fff4; }
134
+ .status-card-display.error { border-color: #fc8181; background: #fffafa; }
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  .progress-bar {
137
  background: #e2e8f0;
 
140
  margin: 15px 0;
141
  overflow: hidden;
142
  }
 
143
  .progress-fill {
144
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
145
  height: 100%;
 
147
  border-radius: 10px;
148
  }
149
 
150
+ /* --- Section Prévisualisation --- */
151
+ .preview-section {
152
+ display: none; /* Caché par défaut */
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  }
154
+ .image-grid {
155
+ display: grid;
156
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
157
+ gap: 20px;
158
+ margin-top: 20px;
 
 
159
  }
160
+ .image-grid img {
161
+ width: 100%;
162
+ height: auto;
 
 
163
  border-radius: 10px;
164
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
165
+ object-fit: cover;
166
+ aspect-ratio: 2 / 3;
167
+ background-color: #f0f0f0; /* Placeholder color */
 
168
  }
169
 
170
+ /* --- Alertes et Spinner --- */
171
  .spinner {
172
  display: inline-block;
173
  width: 20px;
 
177
  border-top-color: #fff;
178
  animation: spin 1s ease-in-out infinite;
179
  }
180
+ @keyframes spin { to { transform: rotate(360deg); } }
 
 
 
 
181
  .alert {
182
  padding: 15px;
183
  border-radius: 10px;
184
  margin: 15px 0;
185
  font-weight: bold;
186
  }
187
+ .alert-error { background: #fed7d7; color: #c53030; border: 2px solid #fc8181; }
188
+ .alert-success { background: #c6f6d5; color: #2f855a; border: 2px solid #68d391; }
189
 
190
+ /* --- Section d'Exemple JSON --- */
191
+ .example-json {
192
+ background: #2d3748;
193
+ color: #e2e8f0;
194
+ padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  border-radius: 10px;
196
+ font-family: 'Courier New', monospace;
197
+ font-size: 14px;
198
+ overflow-x: auto;
199
+ white-space: pre-wrap;
200
  }
 
 
 
 
 
 
201
 
202
+ /* --- Responsive Design --- */
203
+ @media (max-width: 900px) {
204
+ .main-content { grid-template-columns: 1fr; }
205
+ .header h1 { font-size: 2.5em; }
 
 
 
206
  }
207
  </style>
208
  </head>
209
  <body>
210
  <div class="container">
211
+
212
+ <header class="header">
213
+ <h1>🎨 Générateur de Manga IA</h1>
214
+ <p>Créez votre manga personnalisé avec l'IA Gemini. Décrivez chaque page et laissez la magie opérer !</p>
215
+ </header>
216
+
217
+ <main class="main-content">
218
+ <section id="input-section" class="card">
219
+ <h2>📝 Scénario du Manga</h2>
220
+ <textarea id="jsonInput" class="json-input" placeholder="Collez votre JSON de configuration ici...">{
221
+ "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'.",
222
+ "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.",
223
+ "partie-3": "Action intense. Kai esquive une attaque laser d'un drone robotique. Traits de vitesse et effets d'impact. Style dynamique.",
224
+ "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."
 
 
 
 
225
  }</textarea>
226
  <button id="generateBtn" class="btn">
227
+ <span id="btnIcon">🚀</span>
228
+ <span id="btnText">Générer le Manga</span>
229
  </button>
230
+ </section>
231
+
232
+ <section id="status-section" class="card">
233
+ <h2>📊 Suivi de Génération</h2>
234
  <div id="statusContainer">
235
  <p style="color: #718096; text-align: center; padding: 40px;">
236
+ Prêt à démarrer.<br>
237
+ Décrivez votre scénario et cliquez sur "Générer" pour commencer.
238
  </p>
239
  </div>
240
+ </section>
241
+ </main>
242
+
243
+ <section id="previewSection" class="card preview-section">
 
244
  <h2>🖼️ Prévisualisation des Pages</h2>
245
  <div id="imageGrid" class="image-grid">
246
+ <!-- Les images générées apparaîtront ici dynamiquement -->
247
  </div>
248
+ </section>
249
 
250
+ <section class="card">
251
+ <h2>📋 Format du Scénario (JSON)</h2>
 
252
  <p style="margin-bottom: 20px;">
253
+ Utilisez des clés "partie-X" (où X est un numéro) avec une description détaillée pour chaque page :
254
  </p>
255
  <div class="example-json">{
256
+ "partie-1": "Description détaillée de la première page...",
257
+ "partie-2": "Description détaillée de la deuxième page...",
 
258
  "partie-N": "Continuez avec autant de parties que nécessaire..."
259
  }</div>
260
  <p style="margin-top: 15px; color: #4a5568;">
261
+ <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.
 
 
262
  </p>
263
+ </section>
264
+
265
  </div>
266
 
267
  <script>
268
+ // --- Variables globales ---
269
  let currentTaskId = null;
270
  let statusInterval = null;
271
 
272
+ // --- Éléments du DOM ---
273
  const generateBtn = document.getElementById('generateBtn');
274
  const jsonInput = document.getElementById('jsonInput');
275
  const statusContainer = document.getElementById('statusContainer');
276
  const previewSection = document.getElementById('previewSection');
277
  const imageGrid = document.getElementById('imageGrid');
278
+ const btnIcon = document.getElementById('btnIcon');
279
+ const btnText = document.getElementById('btnText');
280
+
281
+ // --- Écouteurs d'événements ---
282
+ generateBtn.addEventListener('click', startGeneration);
283
+ jsonInput.addEventListener('blur', autoFormatJSON);
284
+ window.addEventListener('beforeunload', () => clearInterval(statusInterval));
285
 
286
+ // --- Fonctions principales ---
287
+ async function startGeneration() {
288
+ // 1. Réinitialiser l'interface
289
+ resetUI();
 
 
290
 
291
+ // 2. Valider le JSON
 
 
 
 
 
292
  let jsonData;
293
  try {
294
+ const jsonText = jsonInput.value.trim();
295
+ if (!jsonText) throw new Error("Veuillez saisir un scénario JSON.");
296
  jsonData = JSON.parse(jsonText);
297
+ if (Object.keys(jsonData).filter(k => k.startsWith('partie-')).length === 0) {
298
+ throw new Error('Aucune "partie-X" trouvée dans le JSON.');
299
+ }
300
  } catch (error) {
301
+ showAlert(`Erreur de format JSON : ${error.message}`, 'error');
 
 
 
 
 
 
 
302
  return;
303
  }
304
+
305
+ // 3. Envoyer la requête de génération
306
  try {
307
+ setButtonState(true, 'Démarrage...');
 
 
308
  const response = await fetch('/generate', {
309
  method: 'POST',
310
+ headers: { 'Content-Type': 'application/json' },
 
 
311
  body: JSON.stringify(jsonData)
312
  });
 
313
  const result = await response.json();
314
 
315
+ if (!response.ok) throw new Error(result.error || 'Erreur inconnue du serveur.');
316
+
317
+ // 4. Démarrer le suivi de la progression
318
+ currentTaskId = result.task_id;
319
+ startStatusPolling();
320
+ showAlert(`Génération démarrée ! Tâche ID : ${currentTaskId}`, 'success');
 
321
 
322
  } catch (error) {
323
+ showAlert(`Erreur de démarrage : ${error.message}`, 'error');
324
+ setButtonState(false);
 
325
  }
326
+ }
327
 
 
328
  function startStatusPolling() {
329
+ clearInterval(statusInterval);
 
 
 
330
  statusInterval = setInterval(async () => {
331
  if (!currentTaskId) return;
 
332
  try {
333
  const response = await fetch(`/status/${currentTaskId}`);
334
  const status = await response.json();
 
335
  if (response.ok) {
336
  updateStatusDisplay(status);
337
+ if (['completed', 'error'].includes(status.status)) {
 
338
  clearInterval(statusInterval);
339
+ setButtonState(false);
 
 
340
  }
341
  } else {
342
+ console.error('Erreur de statut:', status.error);
343
  }
344
  } catch (error) {
345
  console.error('Erreur réseau:', error);
346
  }
347
+ }, 3000); // Vérifier toutes les 3 secondes
348
  }
349
 
 
350
  function updateStatusDisplay(status) {
351
+ // 1. Mettre à jour la carte de statut
352
+ const statusInfo = getStatusInfo(status.status);
353
+ const progressHtml = getProgressHtml(status);
354
+ const downloadHtml = status.status === 'completed' ? getDownloadButtonHtml() : '';
355
+ const errorHtml = status.error ? `<div class="alert alert-error">${status.error}</div>` : '';
356
+
357
+ statusContainer.innerHTML = `
358
+ <div class="status-card-display ${statusInfo.class}">
359
+ <h3>${statusInfo.icon} ${statusInfo.text}</h3>
360
+ <p><small><strong>ID :</strong> ${currentTaskId}</small></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  ${progressHtml}
362
  ${errorHtml}
363
  ${downloadHtml}
364
+ </div>`;
 
365
 
366
+ // 2. Mettre à jour la grille d'images
367
  if (status.image_urls && status.image_urls.length > 0) {
368
  previewSection.style.display = 'block';
 
369
  status.image_urls.forEach(url => {
 
370
  if (!document.querySelector(`img[src="${url}"]`)) {
371
  const img = document.createElement('img');
372
  img.src = url;
373
  img.alt = `Page générée pour la tâche ${currentTaskId}`;
 
374
  img.style.opacity = 0;
375
+ img.onload = () => {
376
+ img.style.transition = 'opacity 0.5s ease-in-out';
377
+ img.style.opacity = 1;
378
+ };
379
  imageGrid.appendChild(img);
380
  }
381
  });
382
  }
383
  }
384
 
385
+ // --- Fonctions utilitaires ---
386
+ function resetUI() {
387
+ imageGrid.innerHTML = '';
388
+ previewSection.style.display = 'none';
389
+ if(statusContainer.querySelector('.alert')) {
390
+ statusContainer.querySelector('.alert').remove();
391
+ }
392
+ }
393
+
394
+ function setButtonState(isLoading, text = 'Générer le Manga') {
395
+ generateBtn.disabled = isLoading;
396
+ btnText.textContent = text;
397
+ btnIcon.innerHTML = isLoading ? '<span class="spinner"></span>' : '🚀';
398
+ }
399
+
400
+ function getStatusInfo(status) {
401
+ const map = {
402
+ 'queued': { icon: '⏳', text: 'En file d\'attente', class: 'generating' },
403
+ 'generating': { icon: '🎨', text: 'Génération en cours...', class: 'generating' },
404
+ 'creating_zip': { icon: '🗜️', text: 'Création de l\'archive ZIP...', class: 'generating' },
405
+ 'completed': { icon: '✅', text: 'Terminé !', class: 'completed' },
406
+ 'error': { icon: '❌', text: 'Erreur', class: 'error' }
407
+ };
408
+ return map[status] || { icon: '❔', text: 'Inconnu', class: '' };
409
+ }
410
+
411
+ function getProgressHtml(status) {
412
+ if (!status.total_pages || status.status === 'completed') return '';
413
+ const progress = (status.current_page / status.total_pages) * 100;
414
+ return `
415
+ <p style="text-align: center; margin-top: 10px;">
416
+ Page ${status.current_page || 0} sur ${status.total_pages}
417
+ </p>
418
+ <div class="progress-bar">
419
+ <div class="progress-fill" style="width: ${progress}%"></div>
420
+ </div>`;
421
+ }
422
+
423
+ function getDownloadButtonHtml() {
424
+ return `<a href="/download/${currentTaskId}" class="btn download-btn">📥 Télécharger l'archive ZIP</a>`;
425
+ }
426
+
427
  function showAlert(message, type) {
428
  const alertClass = type === 'error' ? 'alert-error' : 'alert-success';
429
  const alertHtml = `<div class="alert ${alertClass}">${message}</div>`;
430
+ statusContainer.innerHTML = alertHtml;
 
 
 
 
 
 
 
 
431
  }
432
 
433
+ function autoFormatJSON() {
 
 
 
 
 
 
434
  try {
435
  const parsed = JSON.parse(jsonInput.value);
436
+ jsonInput.value = JSON.stringify(parsed, null, 2); // 2 espaces pour l'indentation
437
+ } catch (e) {
438
+ // Si le JSON est invalide, ne rien faire pour ne pas effacer la saisie de l'utilisateur
439
  }
440
+ }
441
  </script>
442
  </body>
443
  </html>