Docfile commited on
Commit
3aacddd
·
verified ·
1 Parent(s): e22e1db

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +264 -615
templates/index.html CHANGED
@@ -4,363 +4,77 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Résolveur d'Images & PDF - Mariam</title>
7
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.js"></script>
8
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/contrib/auto-render.min.js"></script>
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css">
10
  <style>
11
  :root {
12
- --primary-color: #3498db;
13
- --primary-hover: #2980b9;
14
- --secondary-color: #2ecc71;
15
- --secondary-hover: #27ae60;
16
- --danger-color: #e74c3c;
17
- --danger-hover: #c0392b;
18
- --background-color: #f4f7f6;
19
- --text-color: #333;
20
- --border-color: #e0e0e0;
21
- --shadow: 0 4px 15px rgba(0,0,0,0.1);
22
- --spacing-unit: 1rem;
23
  }
24
-
25
- * {
26
- box-sizing: border-box;
27
- margin: 0;
28
- padding: 0;
29
- }
30
-
31
  body {
32
- font-family: 'Segoe UI', system-ui, sans-serif;
33
- max-width: 800px;
34
- margin: 0 auto;
35
- padding: calc(var(--spacing-unit) * 2);
36
- line-height: 1.6;
37
- background-color: var(--background-color);
38
- color: var(--text-color);
39
- }
40
-
41
- .header {
42
- text-align: center;
43
- margin-bottom: calc(var(--spacing-unit) * 2);
44
- }
45
-
46
- .header h1 {
47
- font-size: 2.5rem;
48
- color: #2c3e50;
49
- margin-bottom: calc(var(--spacing-unit) * 0.5);
50
- }
51
-
52
- .header .subtitle {
53
- font-size: 1.1rem;
54
- color: #555;
55
- }
56
-
57
- .telegram-join-button-container {
58
- text-align: center;
59
- margin-bottom: calc(var(--spacing-unit) * 2);
60
- }
61
-
62
  .telegram-button {
63
- display: inline-block;
64
- background-color: #0088cc;
65
- color: white;
66
- padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
67
- border-radius: 0.5rem;
68
- text-decoration: none;
69
- transition: all 0.3s ease;
70
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
71
- }
72
-
73
- .telegram-button:hover {
74
- transform: translateY(-2px);
75
- background-color: #006699;
76
- }
77
-
78
- .container {
79
- background-color: white;
80
- padding: calc(var(--spacing-unit) * 2);
81
- border-radius: 1rem;
82
- box-shadow: var(--shadow);
83
- }
84
-
85
- .style-selection {
86
- background-color: #f9f9f9;
87
- padding: calc(var(--spacing-unit) * 1.5);
88
- border-radius: 0.75rem;
89
- border: 1px solid var(--border-color);
90
- margin-bottom: calc(var(--spacing-unit) * 1.5);
91
- }
92
-
93
- .style-selection h3 {
94
- margin-bottom: var(--spacing-unit);
95
- color: #2c3e50;
96
- font-size: 1.2rem;
97
- }
98
-
99
- .radio-group {
100
- display: flex;
101
- flex-direction: column;
102
- gap: var(--spacing-unit);
103
- }
104
-
105
- .radio-option {
106
- display: flex;
107
- align-items: flex-start;
108
- padding: calc(var(--spacing-unit) * 0.75);
109
- border-radius: 0.5rem;
110
- transition: background-color 0.2s;
111
- cursor: pointer;
112
- border: 1px solid transparent;
113
- }
114
-
115
- .radio-option:hover {
116
- background-color: #f0f4f8;
117
- border-color: var(--primary-color);
118
- }
119
-
120
- .radio-option input[type="radio"] {
121
- margin-top: 0.25rem;
122
- margin-right: calc(var(--spacing-unit) * 0.75);
123
- width: 1.25rem;
124
- height: 1.25rem;
125
- accent-color: var(--primary-color);
126
- }
127
-
128
- .radio-content {
129
- flex: 1;
130
- }
131
-
132
- .radio-label {
133
- font-weight: 500;
134
- margin-bottom: calc(var(--spacing-unit) * 0.25);
135
- display: block;
136
- }
137
-
138
- .radio-description {
139
- font-size: 0.9rem;
140
- color: #666;
141
- }
142
-
143
- .upload-section {
144
- border: 3px dashed var(--border-color);
145
- padding: calc(var(--spacing-unit) * 2);
146
- text-align: center;
147
- border-radius: 0.75rem;
148
- cursor: pointer;
149
- transition: all 0.3s ease;
150
- background-color: #f8f9fa;
151
- margin: calc(var(--spacing-unit) * 1.5) 0;
152
- }
153
-
154
- .upload-section:hover {
155
- border-color: var(--primary-color);
156
- background-color: #e8f4fb;
157
- }
158
-
159
- .upload-icon {
160
- font-size: 2.5rem;
161
- margin-bottom: var(--spacing-unit);
162
- color: var(--primary-color);
163
- }
164
-
165
- #file-input {
166
- display: none;
167
- }
168
-
169
- #file-preview-area {
170
- margin-top: var(--spacing-unit);
171
- display: flex;
172
- flex-wrap: wrap;
173
- gap: var(--spacing-unit);
174
- justify-content: center;
175
- }
176
-
177
- .preview-item {
178
- display: flex;
179
- flex-direction: column;
180
- align-items: center;
181
- gap: calc(var(--spacing-unit) * 0.5);
182
- padding: calc(var(--spacing-unit) * 0.5);
183
- border: 1px solid var(--border-color);
184
- border-radius: 0.5rem;
185
- background-color: #fdfdfd;
186
- }
187
-
188
- .preview-item img {
189
- max-width: 100px;
190
- max-height: 100px;
191
- border-radius: 0.25rem;
192
- object-fit: cover;
193
- }
194
- .preview-item .pdf-icon {
195
- font-size: 3rem;
196
- color: var(--danger-color);
197
- }
198
- .preview-item span {
199
- font-size: 0.8rem;
200
- color: #555;
201
- word-break: break-all;
202
- max-width: 100px;
203
- text-align: center;
204
- }
205
-
206
- .button {
207
- width: 100%;
208
- padding: var(--spacing-unit);
209
- border: none;
210
- border-radius: 0.5rem;
211
- font-size: 1rem;
212
- cursor: pointer;
213
- transition: all 0.3s ease;
214
- margin: var(--spacing-unit) 0;
215
- background-color: var(--primary-color);
216
- color: white;
217
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
218
- }
219
-
220
- .button:hover:not(:disabled) {
221
- transform: translateY(-2px);
222
- background-color: var(--primary-hover);
223
- }
224
-
225
- .button:disabled {
226
- background-color: #bdc3c7;
227
- cursor: not-allowed;
228
- }
229
-
230
- .clear-button {
231
- background-color: var(--danger-color);
232
- margin-top: 0;
233
- }
234
- .clear-button:hover:not(:disabled) {
235
- background-color: var(--danger-hover);
236
- }
237
-
238
- .copy-button {
239
- background-color: var(--secondary-color);
240
- }
241
-
242
- .copy-button:hover:not(:disabled) {
243
- background-color: var(--secondary-hover);
244
- }
245
-
246
- .cooldown-notice {
247
- background-color: #fff3cd;
248
- border: 1px solid #ffeaa7;
249
- border-radius: 0.5rem;
250
- padding: var(--spacing-unit);
251
- margin: var(--spacing-unit) 0;
252
- text-align: center;
253
- color: #856404;
254
- font-weight: 500;
255
- }
256
-
257
- .cooldown-timer {
258
- font-size: 1.2rem;
259
- color: #d63031;
260
- font-weight: bold;
261
- }
262
-
263
- #solving-container {
264
- display: none;
265
- background-color: #f9f9f9;
266
- padding: calc(var(--spacing-unit) * 1.5);
267
- border-radius: 0.75rem;
268
- border: 1px solid var(--border-color);
269
- margin-top: calc(var(--spacing-unit) * 1.5);
270
- }
271
-
272
- .status {
273
- text-align: center;
274
- margin-bottom: var(--spacing-unit);
275
- font-weight: bold;
276
- color: #2c3e50;
277
- }
278
-
279
- .status.error { color: #e74c3c; }
280
- .status.completed { color: #2ecc71; }
281
-
282
- .telegram-notice {
283
- background-color: #eaf5ff;
284
- border-left: 5px solid var(--primary-color);
285
- padding: var(--spacing-unit);
286
- margin: var(--spacing-unit) 0;
287
- border-radius: 0 0.5rem 0.5rem 0;
288
- }
289
-
290
- .response-container {
291
- display: none;
292
- margin-top: calc(var(--spacing-unit) * 1.5);
293
- padding: calc(var(--spacing-unit) * 1.5);
294
- background-color: white;
295
- border-radius: 0.75rem;
296
- border: 1px solid var(--border-color);
297
- }
298
-
299
- #response {
300
- background-color: #fdfdfd;
301
- padding: var(--spacing-unit);
302
- border-radius: 0.5rem;
303
- border: 1px solid #eee;
304
- min-height: 50px;
305
- white-space: pre-wrap;
306
- word-wrap: break-word;
307
- }
308
-
309
- .loading {
310
- text-align: center;
311
- font-style: italic;
312
- color: #555;
313
- margin: var(--spacing-unit) 0;
314
- }
315
-
316
- .loading::before {
317
- content: "⏳ ";
318
- }
319
-
320
  @media (max-width: 768px) {
321
- :root {
322
- --spacing-unit: 0.875rem;
323
- }
324
-
325
- body {
326
- padding: var(--spacing-unit);
327
- }
328
-
329
- .header h1 {
330
- font-size: 1.75rem;
331
- }
332
-
333
- .container {
334
- padding: var(--spacing-unit);
335
- }
336
-
337
- .radio-option {
338
- padding: calc(var(--spacing-unit) * 0.5);
339
- }
340
-
341
- .radio-content {
342
- font-size: 0.95rem;
343
- }
344
-
345
- .radio-description {
346
- font-size: 0.85rem;
347
- }
348
-
349
- .upload-section {
350
- padding: var(--spacing-unit);
351
- }
352
-
353
- .telegram-button {
354
- padding: calc(var(--spacing-unit) * 0.75) var(--spacing-unit);
355
- font-size: 0.95rem;
356
- }
357
- .preview-item img {
358
- max-width: 80px;
359
- max-height: 80px;
360
- }
361
- .preview-item .pdf-icon {
362
- font-size: 2.5rem;
363
- }
364
  }
365
  </style>
366
  </head>
@@ -372,7 +86,7 @@
372
 
373
  <div class="telegram-join-button-container">
374
  <a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" class="telegram-button">
375
- 🚀 Rejoindre le Groupe Telegram pour obtenir le PDF
376
  </a>
377
  </div>
378
 
@@ -387,12 +101,11 @@
387
  <div class="radio-description">Format simple et épuré, idéal pour une lecture rapide</div>
388
  </div>
389
  </div>
390
-
391
  <div class="radio-option" onclick="selectStyle('colorful')">
392
  <input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked>
393
  <div class="radio-content">
394
  <label class="radio-label" for="style-colorful">🌈 Résolution Colorée</label>
395
- <div class="radio-description">Format richement formaté avec couleurs, boîtes et mise en page élégante</div>
396
  </div>
397
  </div>
398
  </div>
@@ -406,9 +119,7 @@
406
  <div class="upload-icon">📤</div>
407
  <p>Cliquez ou glissez-déposez vos images et/ou 1 fichier PDF ici</p>
408
  <input type="file" id="file-input" accept="image/*,application/pdf" multiple>
409
- <div id="file-preview-area">
410
- <!-- Les aperçus des fichiers seront ajoutés ici -->
411
- </div>
412
  </div>
413
 
414
  <button id="clear-files-button" class="button clear-button" style="display: none;">🗑️ Effacer les fichiers</button>
@@ -417,17 +128,21 @@
417
  <div id="solving-container">
418
  <div class="status" id="status">En attente de résolution...</div>
419
  <div class="telegram-notice">
420
- La réponse complète sera également envoyée sous forme de fichier texte sur notre groupe Telegram.
421
  </div>
422
- <div class="loading" id="loading-text">Traitement en cours...</div>
423
  <div class="response-container" id="response-container">
424
- <h3>Réponse de Mariam :</h3>
425
  <div id="response"></div>
426
- <button id="copy-button" class="button copy-button">📋 Copier la réponse</button>
427
  </div>
428
  </div>
429
  </div>
430
 
 
 
 
 
 
 
431
  <script>
432
  document.addEventListener('DOMContentLoaded', function() {
433
  const uploadSection = document.getElementById('upload-section');
@@ -438,39 +153,111 @@
438
  const solvingContainer = document.getElementById('solving-container');
439
  const responseContainer = document.getElementById('response-container');
440
  const responseDiv = document.getElementById('response');
441
- const copyButton = document.getElementById('copy-button');
442
  const statusElement = document.getElementById('status');
443
- const loadingText = document.getElementById('loading-text');
444
  const cooldownNotice = document.getElementById('cooldown-notice');
445
  const cooldownTimer = document.getElementById('cooldown-timer');
446
-
 
 
447
  let selectedFiles = [];
448
  let cooldownEndTime = 0;
449
  let cooldownInterval = null;
450
-
451
- // Vérifier le cooldown au chargement de la page
452
- checkCooldownOnLoad();
453
-
454
- window.selectStyle = function(style) {
455
- document.getElementById(`style-${style}`).checked = true;
456
- };
457
-
458
- function checkCooldownOnLoad() {
459
- const savedCooldownEndTime = localStorage.getItem('mariamCooldownEndTime');
460
- if (savedCooldownEndTime) {
461
- const endTime = parseInt(savedCooldownEndTime);
462
- const now = Date.now();
463
- if (now < endTime) {
464
- cooldownEndTime = endTime;
465
- startCooldownTimer();
466
- } else {
467
- localStorage.removeItem('mariamCooldownEndTime');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
 
 
 
 
 
 
 
 
 
 
469
  }
470
  }
471
 
472
  function startCooldown() {
473
- cooldownEndTime = Date.now() + (2 * 60 * 1000); // 2 minutes
474
  localStorage.setItem('mariamCooldownEndTime', cooldownEndTime.toString());
475
  startCooldownTimer();
476
  }
@@ -478,136 +265,72 @@
478
  function startCooldownTimer() {
479
  cooldownNotice.style.display = 'block';
480
  solveButton.disabled = true;
481
-
482
  cooldownInterval = setInterval(() => {
483
- const now = Date.now();
484
- const remainingTime = Math.max(0, cooldownEndTime - now);
485
-
486
- if (remainingTime <= 0) {
487
  clearInterval(cooldownInterval);
488
  cooldownNotice.style.display = 'none';
489
- localStorage.removeItem('mariamCooldownEndTime');
490
- updateButtonsState(); // Réactiver le bouton si des fichiers sont sélectionnés
491
  return;
492
  }
493
-
494
- const minutes = Math.floor(remainingTime / 60000);
495
- const seconds = Math.floor((remainingTime % 60000) / 1000);
496
  cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
497
  }, 1000);
498
  }
499
 
500
- function isCooldownActive() {
501
- return Date.now() < cooldownEndTime;
502
- }
503
-
504
- uploadSection.addEventListener('click', () => fileInput.click());
505
-
506
- uploadSection.addEventListener('dragover', (e) => {
507
- e.preventDefault();
508
- uploadSection.style.borderColor = 'var(--primary-color)';
509
- uploadSection.style.backgroundColor = '#e8f4fb';
510
- });
511
-
512
- uploadSection.addEventListener('dragleave', () => {
513
- uploadSection.style.borderColor = 'var(--border-color)';
514
- uploadSection.style.backgroundColor = '#f8f9fa';
515
- });
516
-
517
- uploadSection.addEventListener('drop', (e) => {
518
- e.preventDefault();
519
- uploadSection.style.borderColor = 'var(--border-color)';
520
- uploadSection.style.backgroundColor = '#f8f9fa';
521
-
522
- if (e.dataTransfer.files.length) {
523
- handleFileSelection(e.dataTransfer.files);
524
- }
525
- });
526
-
527
- fileInput.addEventListener('change', (e) => {
528
- if (e.target.files.length) {
529
- handleFileSelection(e.target.files);
530
- }
531
- });
532
 
533
- function handleFileSelection(files) {
534
  const newFiles = Array.from(files);
535
- let pdfAlreadySelected = selectedFiles.some(f => f.type === 'application/pdf');
536
-
537
  newFiles.forEach(file => {
538
  if (file.type.startsWith('image/')) {
539
- if (!selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) {
540
- selectedFiles.push(file);
541
- }
542
  } else if (file.type === 'application/pdf') {
543
- if (!pdfAlreadySelected) {
544
  selectedFiles = selectedFiles.filter(f => f.type !== 'application/pdf');
545
  selectedFiles.push(file);
546
- pdfAlreadySelected = true;
547
- } else {
548
- alert('Vous ne pouvez sélectionner qu\'un seul fichier PDF.');
549
  }
550
- } else {
551
- alert(`Le fichier "${file.name}" n'est pas une image ou un PDF valide et sera ignoré.`);
552
  }
553
  });
554
-
555
  updateFilePreviews();
556
  updateButtonsState();
557
-
558
- solvingContainer.style.display = 'none';
559
- responseContainer.style.display = 'none';
560
- }
 
 
 
561
 
562
  function updateFilePreviews() {
563
  filePreviewArea.innerHTML = '';
564
-
565
- if (selectedFiles.length === 0) {
566
- filePreviewArea.style.display = 'none';
567
- return;
568
- }
569
- filePreviewArea.style.display = 'flex';
570
-
571
  selectedFiles.forEach(file => {
572
- const previewItem = document.createElement('div');
573
- previewItem.classList.add('preview-item');
574
-
575
- const fileNameSpan = document.createElement('span');
576
- fileNameSpan.textContent = file.name.length > 15 ? file.name.substring(0,12) + "..." : file.name;
577
-
578
  if (file.type.startsWith('image/')) {
579
  const img = document.createElement('img');
580
- const reader = new FileReader();
581
- reader.onload = (e) => {
582
- img.src = e.target.result;
583
- };
584
- reader.readAsDataURL(file);
585
- previewItem.appendChild(img);
586
- } else if (file.type === 'application/pdf') {
587
- const pdfIcon = document.createElement('div');
588
- pdfIcon.classList.add('pdf-icon');
589
- pdfIcon.textContent = '📄';
590
- previewItem.appendChild(pdfIcon);
591
  }
592
- previewItem.appendChild(fileNameSpan);
593
- filePreviewArea.appendChild(previewItem);
594
  });
595
  }
596
 
597
  function updateButtonsState() {
598
- if (selectedFiles.length > 0 && !isCooldownActive()) {
599
- solveButton.disabled = false;
600
- solveButton.textContent = `🔍 Résoudre (${selectedFiles.length} fichier(s))`;
601
- clearFilesButton.style.display = 'block';
602
- } else if (selectedFiles.length > 0 && isCooldownActive()) {
603
- solveButton.disabled = true;
604
- solveButton.textContent = `🔍 Résoudre (${selectedFiles.length} fichier(s))`;
605
- clearFilesButton.style.display = 'block';
606
- } else {
607
- solveButton.disabled = true;
608
- solveButton.textContent = '🔍 Résoudre';
609
- clearFilesButton.style.display = 'none';
610
- }
611
  }
612
 
613
  clearFilesButton.addEventListener('click', () => {
@@ -616,171 +339,97 @@
616
  updateFilePreviews();
617
  updateButtonsState();
618
  solvingContainer.style.display = 'none';
619
- responseContainer.style.display = 'none';
620
  });
621
 
622
  solveButton.addEventListener('click', () => {
623
  if (selectedFiles.length === 0 || isCooldownActive()) return;
624
 
625
- const selectedStyle = document.querySelector('input[name="resolution-style"]:checked').value;
626
-
627
- // Démarrer le cooldown immédiatement
628
  startCooldown();
629
-
630
  solveButton.disabled = true;
631
  solveButton.textContent = '⏳ Traitement...';
 
632
  solvingContainer.style.display = 'block';
633
  responseContainer.style.display = 'none';
 
634
  statusElement.className = 'status';
635
- statusElement.textContent = 'Préparation de la requête...';
636
- loadingText.style.display = 'block';
637
  responseDiv.innerHTML = '';
638
-
639
  const formData = new FormData();
640
- selectedFiles.forEach(file => {
641
- formData.append('user_files', file);
642
- });
643
- formData.append('style', selectedStyle);
644
 
645
- fetch('/solve', {
646
- method: 'POST',
647
- body: formData
648
- })
649
  .then(response => {
650
- if (!response.ok) {
651
- return response.json().then(err => { throw new Error(err.error || `Erreur Serveur: ${response.status}`) });
652
- }
653
  return response.json();
654
  })
655
  .then(data => {
656
- if (data.error) {
657
- throw new Error(data.error);
658
- }
 
 
 
659
 
660
- const taskId = data.task_id;
661
- statusElement.textContent = 'Traitement en arrière-plan (ID: ' + taskId + ')';
 
 
 
 
 
 
 
 
 
 
 
 
662
 
663
- const eventSource = new EventSource('/stream/' + taskId);
664
 
665
- eventSource.onmessage = function(event) {
666
- const data = JSON.parse(event.data);
667
-
668
- if (data.error) {
669
- handleError(data.error);
670
- eventSource.close();
671
- return;
672
- }
673
-
674
- updateStatus(data);
675
- if (data.status === 'completed' || data.status === 'error') {
676
- eventSource.close();
677
- }
678
- };
679
 
680
- eventSource.onerror = function() {
 
 
 
 
 
 
681
  eventSource.close();
682
- handleEventSourceError(taskId);
683
- };
684
- })
685
- .catch(error => {
686
- handleError(error.message);
687
- });
688
- });
 
 
 
 
689
 
690
- function handleError(errorMessage) {
691
  statusElement.className = 'status error';
692
  statusElement.textContent = 'Erreur:';
693
  responseDiv.innerHTML = `<p style="color:red;">${errorMessage}</p>`;
694
- showResponse();
695
- }
696
-
697
- function updateStatus(data) {
698
- switch(data.status) {
699
- case 'pending':
700
- statusElement.textContent = 'En file d\'attente...';
701
- break;
702
- case 'processing':
703
- statusElement.innerHTML = '<span class="thinking">Mariam</span> traite vos fichiers... <br><small>La réponse sera également envoyée sur Telegram.</small>';
704
- break;
705
- case 'completed':
706
- statusElement.className = 'status completed';
707
- statusElement.textContent = 'Traitement terminé avec succès ! 🎉';
708
- responseDiv.innerHTML = '<p style="color: #2ecc71; font-size: 1.2rem; text-align: center; font-weight: bold;">📄 Votre PDF est disponible sur Telegram</p>';
709
- showResponse();
710
- break;
711
- case 'error':
712
- handleError(data.error || 'Une erreur inattendue est survenue.');
713
- break;
714
- }
715
- }
716
-
717
- function showResponse() {
718
  responseContainer.style.display = 'block';
719
- loadingText.style.display = 'none';
720
- }
721
-
722
- function handleEventSourceError(taskId) {
723
- fetch('/task/' + taskId)
724
- .then(response => {
725
- if (!response.ok) {
726
- throw new Error(`Erreur serveur lors de la récupération de la tâche: ${response.status}`);
727
- }
728
- return response.json();
729
- })
730
- .then(taskData => {
731
- if (taskData.status === 'completed' && taskData.response) {
732
- updateStatus({
733
- status: 'completed',
734
- response: taskData.response
735
- });
736
- } else if (taskData.status === 'error' || taskData.error) {
737
- handleError(taskData.error || 'Une erreur est survenue lors du traitement de la tâche.');
738
- } else {
739
- handleError('La connexion au flux a été perdue. Vérifiez Telegram ou réessayez plus tard.');
740
- }
741
- })
742
- .catch((err) => {
743
- console.error("Erreur lors de la récupération de l'état de la tâche:", err);
744
- handleError('La connexion au flux a été perdue et la récupération de l\'état final a échoué. La réponse pourrait être sur Telegram.');
745
- });
746
  }
747
-
748
- copyButton.addEventListener('click', () => {
749
- const textToCopy = responseDiv.innerText || responseDiv.textContent;
750
- navigator.clipboard.writeText(textToCopy)
751
- .then(() => {
752
- copyButton.textContent = '✅ Copié!';
753
- setTimeout(() => {
754
- copyButton.textContent = '📋 Copier la réponse';
755
- }, 2000);
756
- })
757
- .catch(() => {
758
- const range = document.createRange();
759
- range.selectNode(responseDiv);
760
- window.getSelection().removeAllRanges();
761
- window.getSelection().addRange(range);
762
- try {
763
- document.execCommand('copy');
764
- copyButton.textContent = '✅ Copié!';
765
- } catch (e) {
766
- copyButton.textContent = '❌ Erreur de copie';
767
- }
768
- window.getSelection().removeAllRanges();
769
- setTimeout(() => {
770
- copyButton.textContent = '📋 Copier la réponse';
771
- }, 2000);
772
- });
773
  });
774
 
775
- renderMathInElement(document.body, {
776
- delimiters: [
777
- {left: '$$', right: '$$', display: true},
778
- {left: '$', right: '$', display: false},
779
- {left: '\\(', right: '\\)', display: false},
780
- {left: '\\[', right: '\\]', display: true}
781
- ],
782
- throwOnError: false
783
- });
784
  });
785
  </script>
786
  </body>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Résolveur d'Images & PDF - Mariam</title>
 
 
 
7
  <style>
8
  :root {
9
+ --primary-color: #3498db; --primary-hover: #2980b9; --secondary-color: #2ecc71;
10
+ --secondary-hover: #27ae60; --danger-color: #e74c3c; --danger-hover: #c0392b;
11
+ --background-color: #f4f7f6; --text-color: #333; --border-color: #e0e0e0;
12
+ --shadow: 0 4px 15px rgba(0,0,0,0.1); --spacing-unit: 1rem;
 
 
 
 
 
 
 
13
  }
14
+ * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
15
  body {
16
+ font-family: 'Segoe UI', system-ui, sans-serif; max-width: 800px; margin: 0 auto;
17
+ padding: calc(var(--spacing-unit) * 2); line-height: 1.6;
18
+ background-color: var(--background-color); color: var(--text-color);
19
+ }
20
+ .header { text-align: center; margin-bottom: calc(var(--spacing-unit) * 2); }
21
+ .header h1 { font-size: 2.5rem; color: #2c3e50; margin-bottom: calc(var(--spacing-unit) * 0.5); }
22
+ .header .subtitle { font-size: 1.1rem; color: #555; }
23
+ .telegram-join-button-container { text-align: center; margin-bottom: calc(var(--spacing-unit) * 2); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  .telegram-button {
25
+ display: inline-block; background-color: #0088cc; color: white;
26
+ padding: var(--spacing-unit) calc(var(--spacing-unit) * 2); border-radius: 0.5rem;
27
+ text-decoration: none; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
28
+ }
29
+ .telegram-button:hover { transform: translateY(-2px); background-color: #006699; }
30
+ .container { background-color: white; padding: calc(var(--spacing-unit) * 2); border-radius: 1rem; box-shadow: var(--shadow); margin-bottom: 2rem; }
31
+ .style-selection { background-color: #f9f9f9; padding: calc(var(--spacing-unit) * 1.5); border-radius: 0.75rem; border: 1px solid var(--border-color); margin-bottom: calc(var(--spacing-unit) * 1.5); }
32
+ .style-selection h3 { margin-bottom: var(--spacing-unit); color: #2c3e50; font-size: 1.2rem; }
33
+ .radio-group { display: flex; flex-direction: column; gap: var(--spacing-unit); }
34
+ .radio-option { display: flex; align-items: flex-start; padding: calc(var(--spacing-unit) * 0.75); border-radius: 0.5rem; transition: background-color 0.2s; cursor: pointer; border: 1px solid transparent; }
35
+ .radio-option:hover { background-color: #f0f4f8; border-color: var(--primary-color); }
36
+ .radio-option input[type="radio"] { margin-top: 0.25rem; margin-right: calc(var(--spacing-unit) * 0.75); width: 1.25rem; height: 1.25rem; accent-color: var(--primary-color); }
37
+ .radio-content { flex: 1; }
38
+ .radio-label { font-weight: 500; margin-bottom: calc(var(--spacing-unit) * 0.25); display: block; }
39
+ .radio-description { font-size: 0.9rem; color: #666; }
40
+ .upload-section { border: 3px dashed var(--border-color); padding: calc(var(--spacing-unit) * 2); text-align: center; border-radius: 0.75rem; cursor: pointer; transition: all 0.3s ease; background-color: #f8f9fa; margin: calc(var(--spacing-unit) * 1.5) 0; }
41
+ .upload-section:hover { border-color: var(--primary-color); background-color: #e8f4fb; }
42
+ .upload-icon { font-size: 2.5rem; margin-bottom: var(--spacing-unit); color: var(--primary-color); }
43
+ #file-input { display: none; }
44
+ #file-preview-area { margin-top: var(--spacing-unit); display: flex; flex-wrap: wrap; gap: var(--spacing-unit); justify-content: center; }
45
+ .preview-item { display: flex; flex-direction: column; align-items: center; gap: calc(var(--spacing-unit) * 0.5); padding: calc(var(--spacing-unit) * 0.5); border: 1px solid var(--border-color); border-radius: 0.5rem; background-color: #fdfdfd; }
46
+ .preview-item img { max-width: 100px; max-height: 100px; border-radius: 0.25rem; object-fit: cover; }
47
+ .preview-item .pdf-icon { font-size: 3rem; color: var(--danger-color); }
48
+ .preview-item span { font-size: 0.8rem; color: #555; word-break: break-all; max-width: 100px; text-align: center; }
49
+ .button { width: 100%; padding: var(--spacing-unit); border: none; border-radius: 0.5rem; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; margin: var(--spacing-unit) 0; background-color: var(--primary-color); color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-decoration: none; display: inline-block; text-align:center; }
50
+ .button:hover:not(:disabled) { transform: translateY(-2px); background-color: var(--primary-hover); }
51
+ .button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
52
+ .clear-button { background-color: var(--danger-color); margin-top: 0; }
53
+ .clear-button:hover:not(:disabled) { background-color: var(--danger-hover); }
54
+ .download-button { background-color: var(--secondary-color); }
55
+ .download-button:hover:not(:disabled) { background-color: var(--secondary-hover); }
56
+ .cooldown-notice { background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 0.5rem; padding: var(--spacing-unit); margin: var(--spacing-unit) 0; text-align: center; color: #856404; font-weight: 500; }
57
+ .cooldown-timer { font-size: 1.2rem; color: #d63031; font-weight: bold; }
58
+ #solving-container { display: none; background-color: #f9f9f9; padding: calc(var(--spacing-unit) * 1.5); border-radius: 0.75rem; border: 1px solid var(--border-color); margin-top: calc(var(--spacing-unit) * 1.5); }
59
+ .status { text-align: center; margin-bottom: var(--spacing-unit); font-weight: bold; color: #2c3e50; }
60
+ .status.error { color: #e74c3c; } .status.completed { color: #2ecc71; }
61
+ .telegram-notice { background-color: #eaf5ff; border-left: 5px solid var(--primary-color); padding: var(--spacing-unit); margin: var(--spacing-unit) 0; border-radius: 0 0.5rem 0.5rem 0; }
62
+ .response-container { display: none; margin-top: calc(var(--spacing-unit) * 1.5); }
63
+ #response { background-color: #fdfdfd; padding: var(--spacing-unit); border-radius: 0.5rem; border: 1px solid #eee; min-height: 50px; white-space: pre-wrap; word-wrap: break-word; }
64
+ #history-container { margin-top: 2rem; }
65
+ #history-container h2 { text-align:center; margin-bottom: 1rem; color: #2c3e50;}
66
+ #history-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem;}
67
+ .history-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: #fff; border: 1px solid var(--border-color); border-radius: 0.5rem; transition: box-shadow 0.2s; }
68
+ .history-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
69
+ .history-info { display: flex; flex-direction: column; }
70
+ .history-filename { font-weight: 500; }
71
+ .history-status { font-size: 0.85rem; }
72
+ .history-status-pending { color: #f39c12; } .history-status-completed { color: var(--secondary-color); } .history-status-error { color: var(--danger-color); }
73
+ .history-actions .button { width: auto; padding: 0.5rem 1rem; font-size: 0.9rem; margin: 0; }
74
+ #clear-history-button { background-color: var(--danger-color); margin-top: 1rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  @media (max-width: 768px) {
76
+ body { padding: var(--spacing-unit); }
77
+ .header h1 { font-size: 1.75rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
  </style>
80
  </head>
 
86
 
87
  <div class="telegram-join-button-container">
88
  <a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" class="telegram-button">
89
+ 🚀 Rejoindre le Groupe Telegram
90
  </a>
91
  </div>
92
 
 
101
  <div class="radio-description">Format simple et épuré, idéal pour une lecture rapide</div>
102
  </div>
103
  </div>
 
104
  <div class="radio-option" onclick="selectStyle('colorful')">
105
  <input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked>
106
  <div class="radio-content">
107
  <label class="radio-label" for="style-colorful">🌈 Résolution Colorée</label>
108
+ <div class="radio-description">Format richement formaté avec couleurs et mise en page élégante</div>
109
  </div>
110
  </div>
111
  </div>
 
119
  <div class="upload-icon">📤</div>
120
  <p>Cliquez ou glissez-déposez vos images et/ou 1 fichier PDF ici</p>
121
  <input type="file" id="file-input" accept="image/*,application/pdf" multiple>
122
+ <div id="file-preview-area"></div>
 
 
123
  </div>
124
 
125
  <button id="clear-files-button" class="button clear-button" style="display: none;">🗑️ Effacer les fichiers</button>
 
128
  <div id="solving-container">
129
  <div class="status" id="status">En attente de résolution...</div>
130
  <div class="telegram-notice">
131
+ Votre PDF sera disponible au téléchargement ici-même et dans votre historique une fois le traitement terminé.
132
  </div>
 
133
  <div class="response-container" id="response-container">
 
134
  <div id="response"></div>
135
+ <a id="download-button" class="button download-button" style="display: none;">📥 Télécharger le PDF</a>
136
  </div>
137
  </div>
138
  </div>
139
 
140
+ <div id="history-container" class="container">
141
+ <h2>Historique des Tâches</h2>
142
+ <ul id="history-list"></ul>
143
+ <button id="clear-history-button" class="button">🗑️ Vider l'historique</button>
144
+ </div>
145
+
146
  <script>
147
  document.addEventListener('DOMContentLoaded', function() {
148
  const uploadSection = document.getElementById('upload-section');
 
153
  const solvingContainer = document.getElementById('solving-container');
154
  const responseContainer = document.getElementById('response-container');
155
  const responseDiv = document.getElementById('response');
 
156
  const statusElement = document.getElementById('status');
157
+ const downloadButton = document.getElementById('download-button');
158
  const cooldownNotice = document.getElementById('cooldown-notice');
159
  const cooldownTimer = document.getElementById('cooldown-timer');
160
+ const historyList = document.getElementById('history-list');
161
+ const clearHistoryButton = document.getElementById('clear-history-button');
162
+
163
  let selectedFiles = [];
164
  let cooldownEndTime = 0;
165
  let cooldownInterval = null;
166
+ const eventSources = {};
167
+
168
+ const getHistory = () => JSON.parse(localStorage.getItem('mariamTaskHistory')) || [];
169
+ const saveHistory = (history) => localStorage.setItem('mariamTaskHistory', JSON.stringify(history));
170
+
171
+ function renderHistory() {
172
+ historyList.innerHTML = '';
173
+ const history = getHistory();
174
+ if (history.length === 0) {
175
+ historyList.innerHTML = '<p style="text-align:center; color:#777;">Aucune tâche dans votre historique.</p>';
176
+ clearHistoryButton.style.display = 'none';
177
+ return;
178
+ }
179
+ clearHistoryButton.style.display = 'block';
180
+
181
+ history.sort((a, b) => b.timestamp - a.timestamp).forEach(task => {
182
+ const li = document.createElement('li');
183
+ li.classList.add('history-item');
184
+ li.dataset.taskId = task.id;
185
+
186
+ let statusText = 'En attente...';
187
+ let statusClass = 'history-status-pending';
188
+ if (task.status === 'completed') {
189
+ statusText = 'Terminé';
190
+ statusClass = 'history-status-completed';
191
+ } else if (task.status === 'error') {
192
+ statusText = 'Erreur';
193
+ statusClass = 'history-status-error';
194
+ } else if (task.status && task.status.startsWith('generating')) {
195
+ statusText = 'Génération en cours...';
196
+ } else if (task.status) {
197
+ statusText = task.status.charAt(0).toUpperCase() + task.status.slice(1);
198
+ }
199
+
200
+ li.innerHTML = `
201
+ <div class="history-info">
202
+ <span class="history-filename">${task.filename}</span>
203
+ <small class="history-status ${statusClass}">${statusText} - ${new Date(task.timestamp).toLocaleString('fr-FR')}</small>
204
+ </div>
205
+ <div class="history-actions" id="actions-${task.id}"></div>
206
+ `;
207
+ historyList.appendChild(li);
208
+ updateHistoryItemActions(task);
209
+ });
210
+ }
211
+
212
+ function updateHistoryItemActions(task) {
213
+ const container = document.getElementById(`actions-${task.id}`);
214
+ if (!container) return;
215
+
216
+ if (task.status === 'completed' && task.download_url) {
217
+ container.innerHTML = `<a href="${task.download_url}" class="button download-button">📥 Télécharger</a>`;
218
+ } else if (task.status === 'error') {
219
+ container.innerHTML = `<span style="color:var(--danger-color); font-weight:bold;">Échec</span>`;
220
+ } else {
221
+ container.innerHTML = `<span style="color:var(--primary-color); font-style:italic;">En cours...</span>`;
222
+ }
223
+ }
224
+
225
+ function updateTaskInHistory(taskId, updates) {
226
+ let history = getHistory();
227
+ const taskIndex = history.findIndex(t => t.id === taskId);
228
+ if (taskIndex > -1) {
229
+ history[taskIndex] = { ...history[taskIndex], ...updates };
230
+ saveHistory(history);
231
+ renderHistory();
232
+ }
233
+ }
234
+
235
+ function checkHistoryStatus() {
236
+ getHistory().forEach(task => {
237
+ if (task.status && !['completed', 'error'].includes(task.status)) {
238
+ fetch(`/task/${task.id}`)
239
+ .then(response => response.json())
240
+ .then(data => {
241
+ if (data.status && data.status !== task.status) {
242
+ updateTaskInHistory(task.id, { status: data.status, download_url: data.download_url, error: data.error });
243
+ }
244
+ }).catch(err => console.error(`Could not check status for ${task.id}:`, err));
245
  }
246
+ });
247
+ }
248
+
249
+ window.selectStyle = (style) => document.getElementById(`style-${style}`).checked = true;
250
+
251
+ function checkCooldownOnLoad() {
252
+ const savedCooldown = localStorage.getItem('mariamCooldownEndTime');
253
+ if (savedCooldown && parseInt(savedCooldown) > Date.now()) {
254
+ cooldownEndTime = parseInt(savedCooldown);
255
+ startCooldownTimer();
256
  }
257
  }
258
 
259
  function startCooldown() {
260
+ cooldownEndTime = Date.now() + 2 * 60 * 1000;
261
  localStorage.setItem('mariamCooldownEndTime', cooldownEndTime.toString());
262
  startCooldownTimer();
263
  }
 
265
  function startCooldownTimer() {
266
  cooldownNotice.style.display = 'block';
267
  solveButton.disabled = true;
268
+ if (cooldownInterval) clearInterval(cooldownInterval);
269
  cooldownInterval = setInterval(() => {
270
+ const remaining = Math.max(0, cooldownEndTime - Date.now());
271
+ if (remaining <= 0) {
 
 
272
  clearInterval(cooldownInterval);
273
  cooldownNotice.style.display = 'none';
274
+ updateButtonsState();
 
275
  return;
276
  }
277
+ const minutes = Math.floor(remaining / 60000);
278
+ const seconds = Math.floor((remaining % 60000) / 1000);
 
279
  cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
280
  }, 1000);
281
  }
282
 
283
+ const isCooldownActive = () => Date.now() < cooldownEndTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
+ const handleFileSelection = (files) => {
286
  const newFiles = Array.from(files);
287
+ let pdfSelected = selectedFiles.some(f => f.type === 'application/pdf');
 
288
  newFiles.forEach(file => {
289
  if (file.type.startsWith('image/')) {
290
+ if (!selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) selectedFiles.push(file);
 
 
291
  } else if (file.type === 'application/pdf') {
292
+ if (!pdfSelected) {
293
  selectedFiles = selectedFiles.filter(f => f.type !== 'application/pdf');
294
  selectedFiles.push(file);
295
+ pdfSelected = true;
 
 
296
  }
 
 
297
  }
298
  });
 
299
  updateFilePreviews();
300
  updateButtonsState();
301
+ };
302
+
303
+ uploadSection.addEventListener('click', () => fileInput.click());
304
+ uploadSection.addEventListener('dragover', (e) => { e.preventDefault(); uploadSection.classList.add('hover'); });
305
+ uploadSection.addEventListener('dragleave', (e) => uploadSection.classList.remove('hover'));
306
+ uploadSection.addEventListener('drop', (e) => { e.preventDefault(); uploadSection.classList.remove('hover'); if (e.dataTransfer.files.length) handleFileSelection(e.dataTransfer.files); });
307
+ fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFileSelection(e.target.files); });
308
 
309
  function updateFilePreviews() {
310
  filePreviewArea.innerHTML = '';
311
+ if (selectedFiles.length === 0) return;
 
 
 
 
 
 
312
  selectedFiles.forEach(file => {
313
+ const item = document.createElement('div');
314
+ item.className = 'preview-item';
315
+ const name = document.createElement('span');
316
+ name.textContent = file.name.length > 15 ? file.name.substring(0, 12) + "..." : file.name;
 
 
317
  if (file.type.startsWith('image/')) {
318
  const img = document.createElement('img');
319
+ img.src = URL.createObjectURL(file);
320
+ item.appendChild(img);
321
+ } else {
322
+ item.innerHTML = '<div class="pdf-icon">📄</div>';
 
 
 
 
 
 
 
323
  }
324
+ item.appendChild(name);
325
+ filePreviewArea.appendChild(item);
326
  });
327
  }
328
 
329
  function updateButtonsState() {
330
+ const hasFiles = selectedFiles.length > 0;
331
+ solveButton.disabled = !hasFiles || isCooldownActive();
332
+ solveButton.textContent = hasFiles ? `🔍 Résoudre (${selectedFiles.length} fichier(s))` : '🔍 Résoudre';
333
+ clearFilesButton.style.display = hasFiles ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
334
  }
335
 
336
  clearFilesButton.addEventListener('click', () => {
 
339
  updateFilePreviews();
340
  updateButtonsState();
341
  solvingContainer.style.display = 'none';
 
342
  });
343
 
344
  solveButton.addEventListener('click', () => {
345
  if (selectedFiles.length === 0 || isCooldownActive()) return;
346
 
 
 
 
347
  startCooldown();
 
348
  solveButton.disabled = true;
349
  solveButton.textContent = '⏳ Traitement...';
350
+
351
  solvingContainer.style.display = 'block';
352
  responseContainer.style.display = 'none';
353
+ downloadButton.style.display = 'none';
354
  statusElement.className = 'status';
355
+ statusElement.textContent = 'Préparation...';
 
356
  responseDiv.innerHTML = '';
357
+
358
  const formData = new FormData();
359
+ selectedFiles.forEach(file => formData.append('user_files', file));
360
+ formData.append('style', document.querySelector('input[name="resolution-style"]:checked').value);
 
 
361
 
362
+ fetch('/solve', { method: 'POST', body: formData })
 
 
 
363
  .then(response => {
364
+ if (!response.ok) return response.json().then(err => { throw new Error(err.error) });
 
 
365
  return response.json();
366
  })
367
  .then(data => {
368
+ const { task_id, first_filename } = data;
369
+
370
+ let history = getHistory();
371
+ history.push({ id: task_id, filename: first_filename, status: 'pending', timestamp: Date.now() });
372
+ saveHistory(history);
373
+ renderHistory();
374
 
375
+ statusElement.textContent = 'Traitement en arrière-plan (ID: ' + task_id.substring(0, 8) + '...)';
376
+ listenToTask(task_id);
377
+ })
378
+ .catch(error => handleError(error.message));
379
+ });
380
+
381
+ function listenToTask(taskId) {
382
+ if (eventSources[taskId]) eventSources[taskId].close();
383
+
384
+ const eventSource = new EventSource('/stream/' + taskId);
385
+ eventSources[taskId] = eventSource;
386
+
387
+ eventSource.onmessage = function(event) {
388
+ const data = JSON.parse(event.data);
389
 
390
+ updateTaskInHistory(taskId, { status: data.status, download_url: data.download_url, error: data.error });
391
 
392
+ statusElement.textContent = `Statut: ${data.status}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
+ if (data.status === 'completed') {
395
+ statusElement.className = 'status completed';
396
+ statusElement.textContent = 'Traitement terminé avec succès ! 🎉';
397
+ responseDiv.innerHTML = `<p style="color: #2ecc71; text-align: center;">Votre PDF est prêt.</p>`;
398
+ downloadButton.href = data.download_url;
399
+ downloadButton.style.display = 'block';
400
+ responseContainer.style.display = 'block';
401
  eventSource.close();
402
+ } else if (data.status === 'error') {
403
+ handleError(data.error || 'Une erreur inattendue est survenue.', taskId);
404
+ eventSource.close();
405
+ }
406
+ };
407
+
408
+ eventSource.onerror = function() {
409
+ eventSource.close();
410
+ checkHistoryStatus();
411
+ };
412
+ }
413
 
414
+ function handleError(errorMessage, taskId = null) {
415
  statusElement.className = 'status error';
416
  statusElement.textContent = 'Erreur:';
417
  responseDiv.innerHTML = `<p style="color:red;">${errorMessage}</p>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  responseContainer.style.display = 'block';
419
+ downloadButton.style.display = 'none';
420
+ if (taskId) updateTaskInHistory(taskId, { status: 'error', error: errorMessage });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  }
422
+
423
+ clearHistoryButton.addEventListener('click', () => {
424
+ if(confirm("Êtes-vous sûr de vouloir vider tout l'historique ? Cette action est irréversible.")) {
425
+ localStorage.removeItem('mariamTaskHistory');
426
+ renderHistory();
427
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  });
429
 
430
+ checkCooldownOnLoad();
431
+ renderHistory();
432
+ checkHistoryStatus();
 
 
 
 
 
 
433
  });
434
  </script>
435
  </body>