Docfile commited on
Commit
d1f8aa4
·
verified ·
1 Parent(s): d191a18

Update templates/maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +168 -197
templates/maj.html CHANGED
@@ -1,4 +1,3 @@
1
-
2
  <!DOCTYPE html>
3
  <html lang="fr">
4
  <head>
@@ -49,6 +48,8 @@
49
 
50
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
51
  .blue-button:hover { background: #2563eb; }
 
 
52
 
53
  .loader {
54
  width: 48px;
@@ -92,7 +93,6 @@
92
  th { background-color: #f3f4f6; font-weight: 600; }
93
  .table-responsive { overflow-x: auto; }
94
 
95
- /* Style pour le bouton Sauvegarder afin de le mettre en évidence */
96
  #saveButton {
97
  background: #3b82f6;
98
  color: white;
@@ -102,7 +102,6 @@
102
  }
103
  #saveButton:hover { background: #2563eb; }
104
 
105
- /* Modal plein écran pour les sauvegardes */
106
  #savedModal {
107
  display: none;
108
  position: fixed;
@@ -118,7 +117,6 @@
118
  overflow-y: auto;
119
  }
120
 
121
- /* Styles spécifiques pour le code et son exécution */
122
  pre {
123
  background-color: #f8f8f8;
124
  border: 1px solid #e2e8f0;
@@ -142,7 +140,6 @@
142
  white-space: pre-wrap;
143
  }
144
 
145
- /* Styles pour les types de contenu spécifiques */
146
  .content-text {}
147
  .content-code { padding: 0; }
148
  .content-result {
@@ -168,7 +165,6 @@
168
 
169
  <main id="mainContent">
170
  <form id="problemForm" class="space-y-6" novalidate>
171
- <!-- Zone de dépôt / sélection d'image -->
172
  <div class="uploadArea p-8 text-center relative" aria-label="Zone de dépôt d'image">
173
  <input type="file" id="imageInput" name="image" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
174
  <div class="space-y-3">
@@ -182,22 +178,19 @@
182
  <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
183
  </div>
184
  </div>
185
- <!-- Aperçu de l'image -->
186
  <div id="imagePreview" class="hidden text-center">
187
  <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
188
  </div>
189
- <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
190
  Résoudre le problème
191
  </button>
192
  </form>
193
 
194
- <!-- Loader -->
195
  <div id="loader" class="hidden mt-8 text-center">
196
  <span class="loader"></span>
197
  <p class="mt-4 text-gray-600">Analyse en cours...</p>
198
  </div>
199
 
200
- <!-- Zone d'affichage de la solution -->
201
  <section id="solution" class="hidden mt-8 space-y-6 relative">
202
  <div class="border-t pt-4">
203
  <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
@@ -211,7 +204,6 @@
211
  <div class="border-t pt-6">
212
  <div class="flex justify-between items-center">
213
  <h3 class="text-xl font-bold text-gray-800 mb-4">Solution</h3>
214
- <!-- Bouton Sauvegarder mis en évidence -->
215
  <button id="saveButton">Sauvegarder</button>
216
  </div>
217
  <div id="answerContent" class="text-gray-700 table-responsive"></div>
@@ -220,7 +212,6 @@
220
  </main>
221
  </div>
222
 
223
- <!-- Modal plein écran pour les sauvegardes -->
224
  <div id="savedModal">
225
  <div id="savedModalContent" class="p-6">
226
  <header class="flex justify-between items-center border-b pb-4">
@@ -229,7 +220,6 @@
229
  </header>
230
  <div id="savedListContainer" class="mt-4">
231
  <ul id="savedList" class="space-y-4">
232
- <!-- Liste des sauvegardes insérée dynamiquement -->
233
  </ul>
234
  </div>
235
  <div class="mt-6">
@@ -242,7 +232,6 @@
242
 
243
  <script>
244
  document.addEventListener('DOMContentLoaded', () => {
245
- // Récupération des éléments
246
  const form = document.getElementById('problemForm');
247
  const imageInput = document.getElementById('imageInput');
248
  const loader = document.getElementById('loader');
@@ -260,7 +249,8 @@
260
  const savedModal = document.getElementById('savedModal');
261
  const savedList = document.getElementById('savedList');
262
  const newExercise = document.getElementById('newExercise');
263
- const mainContent = document.getElementById('mainContent'); // This variable is not used in the original logic, keeping it but it could be removed.
 
264
 
265
  let startTime = null;
266
  let timerInterval = null;
@@ -268,11 +258,70 @@
268
  let answerBuffer = '';
269
  let currentMode = null;
270
  let updateTimeout = null;
271
- // Removed currentEndpoint variable as it's no longer needed
272
 
273
- // Gestion des onglets pour le choix du modèle (Removed as tabs are removed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- // Mise à jour du temps écoulé
276
  const updateTimestamp = () => {
277
  if (startTime) {
278
  const seconds = Math.floor((Date.now() - startTime) / 1000);
@@ -280,9 +329,9 @@
280
  }
281
  };
282
  const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
283
- const stopTimer = () => { clearInterval(timerInterval); startTime = null; timestamp.textContent = ''; };
 
284
 
285
- // Affichage de l'image sélectionnée
286
  const handleFileSelect = file => {
287
  if (!file) return;
288
  const reader = new FileReader();
@@ -296,26 +345,24 @@
296
  thoughtsToggle.addEventListener('click', () => { thoughtsBox.classList.toggle('open'); });
297
  imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
298
 
299
- // Gestion du glisser-déposer
300
  const dropZone = document.querySelector('.uploadArea');
301
  dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400'); });
302
  dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); });
303
  dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); handleFileSelect(e.dataTransfer.files[0]); });
304
 
305
- // Fonction pour appliquer la coloration syntaxique
306
  const applyHighlighting = () => {
307
  document.querySelectorAll('pre code').forEach((block) => {
308
- hljs.highlightBlock(block);
309
  });
310
  };
311
 
312
- // Rendu MathJax et mise à jour de l'affichage
313
  const typesetAnswerIfReady = async () => {
314
  if (window.mathJaxReady) {
315
- MathJax.startup.document.elements = [document.getElementById('answerContent')];
316
  await MathJax.typesetPromise();
317
  applyHighlighting();
318
  answerContent.scrollTop = answerContent.scrollHeight;
 
319
  } else { setTimeout(typesetAnswerIfReady, 200); }
320
  };
321
 
@@ -334,41 +381,33 @@
334
  highlight: function(code, lang) {
335
  if (lang && hljs.getLanguage(lang)) {
336
  try {
337
- return hljs.highlight(code, { language: lang }).value;
338
- } catch (error) {}
339
  }
340
- return code;
341
  }
342
  });
343
 
344
- // Creation of content elements (This part wasn't directly used in the streaming logic before, keeping it just in case it was intended for future use, but the current streaming logic builds markdown directly)
345
- const createContentElement = (content, type) => {
346
- const div = document.createElement('div');
347
- div.className = `content-${type}`;
348
-
349
- switch(type) {
350
- case 'text':
351
- div.innerHTML = marked.parse(content);
352
- break;
353
- case 'code':
354
- div.innerHTML = `<pre><code>${content}</code></pre>`;
355
- break;
356
- case 'result':
357
- div.innerHTML = `<div class="code-execution-result">${content}</div>`;
358
- break;
359
- case 'image':
360
- div.innerHTML = `<img src="data:image/png;base64,${content}" />`;
361
- break;
362
- default:
363
- div.innerHTML = marked.parse(content);
364
- }
365
-
366
- return div;
367
- };
368
-
369
- // Envoi de l'image pour résolution
370
  form.addEventListener('submit', async e => {
371
  e.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  const file = imageInput.files[0];
373
  if (!file) {
374
  Swal.fire({
@@ -379,6 +418,10 @@
379
  return;
380
  }
381
 
 
 
 
 
382
  startTimer();
383
  loader.classList.remove('hidden');
384
  solutionSection.classList.add('hidden');
@@ -386,16 +429,13 @@
386
  answerContent.innerHTML = '';
387
  thoughtsBuffer = '';
388
  answerBuffer = '';
389
- currentMode = null; // Reset mode
390
-
391
- // thoughtsBox starts open by default, keep it open during processing
392
  thoughtsBox.classList.add('open');
393
 
394
  const formData = new FormData();
395
  formData.append('image', file);
396
 
397
  try {
398
- // Hardcode the endpoint to /solved
399
  const response = await fetch('/solve', { method: 'POST', body: formData });
400
 
401
  if (!response.body) {
@@ -404,16 +444,16 @@
404
 
405
  const reader = response.body.getReader();
406
  const decoder = new TextDecoder();
407
- let buffer = '';
408
 
409
  const processChunk = async chunk => {
410
- buffer += decoder.decode(chunk, { stream: true });
411
- const lines = buffer.split('\n\n');
412
- buffer = lines.pop();
413
 
414
  for (const line of lines) {
415
  if (!line.startsWith('data:')) {
416
- console.warn('Skipping non-data line:', line); // Log non-data lines
417
  continue;
418
  }
419
  try {
@@ -425,100 +465,79 @@
425
  solutionSection.classList.remove('hidden');
426
  }
427
 
428
- if (data.content !== undefined) { // Check for undefined to allow empty strings
429
- // Gestion différenciée selon le type de contenu
430
  if (currentMode === 'thinking') {
431
- // For thinking mode, just append text
432
  thoughtsBuffer += data.content;
433
  } else if (currentMode === 'answering') {
434
- // For answering mode, handle different types
435
  switch(data.type) {
436
  case 'code':
437
- answerBuffer += "\n```\n" + data.content + "\n```\n";
438
  break;
439
  case 'result':
440
- // Format results clearly, handling multiple lines
441
  const formattedResult = data.content.split('\n').map(line => `> ${line}`).join('\n');
442
  answerBuffer += "\n" + formattedResult + "\n";
443
  break;
444
  case 'image':
445
- // Include images as markdown images
446
- answerBuffer += `\n![Résultat](data:image/png;base64,${data.content})\n`;
447
  break;
448
- case 'text': // Explicitly handle text within answering mode
449
- default: // Default behavior is to append text
450
  answerBuffer += data.content;
451
  break;
452
  }
453
  }
454
  }
455
-
456
- if (data.error) {
457
- answerBuffer += `\n**Erreur:** ${data.error}\n`;
458
- }
459
-
460
  } catch (e) {
461
  console.error('Error parsing JSON data:', line.slice(5), e);
462
- // Optionally append an error message to the output
463
- if (currentMode === 'thinking') { thoughtsBuffer += `\n[Parsing Error]`; }
464
- else { answerBuffer += `\n[Parsing Error]`; }
465
  }
466
  }
467
-
468
- scheduleUpdate(); // Schedule display update after processing chunks
469
  };
470
 
471
- // Start processing the stream
472
  while (true) {
473
  const { done, value } = await reader.read();
474
  if (done) {
475
- // Process any remaining buffer
476
- if (buffer) {
477
- try {
478
- // Attempt to parse the last bit if it looks like a data line
479
- if (buffer.startsWith('data:')) {
480
- const data = JSON.parse(buffer.slice(5));
481
- if (data.content !== undefined) {
482
- if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
483
- else if (currentMode === 'answering') { answerBuffer += data.content; }
484
- }
485
- } else {
486
- // Handle cases where the buffer is not a complete data line (e.g., partial line)
487
- console.warn('Remaining buffer is not a complete data line:', buffer);
488
- // Decide how to handle partial data - maybe discard or append as raw? Appending as raw might break markdown. Discarding is safer.
489
- }
490
- } catch (e) {
491
- console.error('Error processing final buffer:', e);
492
- }
493
  }
494
- scheduleUpdate(); // Final display update
495
- break; // Exit loop
496
  }
497
  await processChunk(value);
498
  }
499
-
500
- stopTimer(); // Stop timer when stream is done
501
  } catch (error) {
502
  console.error('Erreur de Fetch ou du Stream:', error);
503
  Swal.fire({
504
  icon: 'error',
505
  title: 'Erreur de connexion ou de traitement',
506
- text: `Une erreur est survenue lors du traitement de votre demande: ${error.message}`
507
  });
508
  loader.classList.add('hidden');
509
- stopTimer(); // Ensure timer stops on error
510
- solutionSection.classList.remove('hidden'); // Show section maybe with error message
511
- if(answerBuffer === '') { // If no output yet, put error in answer section
512
  answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
513
- } else { // If some output exists, append error
514
- answerBuffer += `\n\n<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
515
- scheduleUpdate(); // Update display with error
516
  }
 
517
  }
518
  });
519
 
520
 
521
- // Sauvegarde de la solution avec SweetAlert2
522
  saveButton.addEventListener('click', () => {
523
  Swal.fire({
524
  title: 'Sauvegarder la solution',
@@ -527,54 +546,45 @@
527
  inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
528
  showCancelButton: true,
529
  confirmButtonText: 'Sauvegarder',
530
- cancelButtonText: 'Annuler'
 
 
 
531
  }).then((result) => {
532
  if (result.isConfirmed && result.value) {
533
  const saveName = result.value;
534
  const saveData = {
535
- answer: answerContent.innerHTML, // Save the rendered HTML
536
- thinking: thoughtsContent.innerHTML, // Save the rendered HTML
537
  date: new Date().toLocaleString()
538
  };
539
  let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
540
  savedExercises[saveName] = saveData;
541
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
542
- Swal.fire({
543
- icon: 'success',
544
- title: 'Sauvegarde réussie',
545
- text: 'Votre solution a bien été sauvegardée !',
546
- timer: 2000,
547
- showConfirmButton: false
548
- });
549
  }
550
  });
551
  });
552
 
553
- // Chargement des sauvegardes dans le modal
554
  const loadSavedList = () => {
555
  savedList.innerHTML = '';
556
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
557
-
558
  if (Object.keys(savedExercises).length === 0) {
559
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
560
  return;
561
  }
562
-
563
- // Sort by date, newest first (optional but nice)
564
  const sortedEntries = Object.entries(savedExercises).sort(([,a], [,b]) => new Date(b.date) - new Date(a.date));
565
-
566
-
567
  for (const [name, data] of sortedEntries) {
568
  const li = document.createElement('li');
569
- li.className = 'border-b pb-2';
570
  li.innerHTML = `
571
  <div class="flex justify-between items-center">
572
- <button class="text-left text-blue-600 hover:underline" data-save="${name}">
573
- ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
574
  </button>
575
- <button class="text-red-500 hover:text-red-700" data-delete="${name}">
576
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
577
- <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
578
  </svg>
579
  </button>
580
  </div>
@@ -583,111 +593,72 @@
583
  }
584
  };
585
 
586
- // Gestion des clics sur les sauvegardes
587
  savedList.addEventListener('click', (e) => {
588
- // Chargement d'une sauvegarde
589
- if (e.target && e.target.dataset.save) {
590
- const saveName = e.target.dataset.save;
 
 
591
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
592
  const data = savedExercises[saveName];
593
  if (data) {
594
- // Hide the form and loader, show the solution section
595
  form.classList.add('hidden');
596
  loader.classList.add('hidden');
597
  solutionSection.classList.remove('hidden');
598
-
599
- // Load the saved HTML content
600
  thoughtsContent.innerHTML = data.thinking;
601
  answerContent.innerHTML = data.answer;
602
-
603
- // Close the modal
604
  savedModal.classList.remove('active');
605
-
606
- // Re-render MathJax and apply highlighting to the loaded HTML
607
- // Use MathJax.typesetPromise() on the specific elements
608
  if (window.mathJaxReady) {
609
- MathJax.startup.document.elements = [thoughtsContent, answerContent]; // Target both potentially
610
- MathJax.typesetPromise().then(() => {
611
- applyHighlighting(); // Apply highlighting after MathJax
612
- }).catch((err) => console.error("MathJax typesetting failed:", err));
613
- } else {
614
- console.warn("MathJax not ready yet, cannot typeset saved content.");
615
- }
616
-
617
- // Ensure thoughts box is open when viewing a saved solution
618
  thoughtsBox.classList.add('open');
619
-
620
- // Reset buffers and timer as this is static content
621
- thoughtsBuffer = ''; // Or load from saved data if needed for re-processing
622
- answerBuffer = ''; // Or load from saved data if needed for re-processing
623
- stopTimer();
624
- timestamp.textContent = data.date; // Show saved date as timestamp
625
  }
626
- }
627
-
628
- // Suppression d'une sauvegarde
629
- if (e.target && (e.target.dataset.delete || e.target.closest('[data-delete]'))) {
630
- const deleteName = e.target.dataset.delete || e.target.closest('[data-delete]').dataset.delete;
631
-
632
  Swal.fire({
633
  title: 'Êtes-vous sûr ?',
634
  text: "Cette sauvegarde sera définitivement supprimée.",
635
  icon: 'warning',
636
  showCancelButton: true,
637
- confirmButtonColor: '#3085d6',
638
- cancelButtonColor: '#d33',
639
- confirmButtonText: 'Oui, supprimer',
640
  cancelButtonText: 'Annuler'
641
  }).then((result) => {
642
  if (result.isConfirmed) {
643
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
644
  delete savedExercises[deleteName];
645
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
646
-
647
- Swal.fire(
648
- 'Supprimé !',
649
- 'La sauvegarde a été supprimée.',
650
- 'success'
651
- );
652
-
653
- loadSavedList(); // Refresh the list in the modal
654
- // If the currently viewed solution is the one being deleted, clear it
655
- // (Optional, maybe better to just let them view it until they start a new one)
656
  }
657
  });
658
  }
659
  });
660
 
661
- // Ouverture / fermeture du modal de sauvegardes
662
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
663
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
664
 
665
- // Bouton présent uniquement dans le modal pour lancer un nouvel exercice
666
  newExercise.addEventListener('click', () => {
667
- // Reset form and hide solution
668
  form.reset();
669
  form.classList.remove('hidden');
670
  solutionSection.classList.add('hidden');
671
- imagePreview.classList.add('hidden'); // Hide image preview
672
- previewImage.src = ''; // Clear image source
673
-
674
- // Clear content areas and buffers
675
  thoughtsContent.innerHTML = '';
676
  answerContent.innerHTML = '';
677
  thoughtsBuffer = '';
678
  answerBuffer = '';
679
- currentMode = null; // Reset mode
680
-
681
- // Stop timer and clear timestamp
682
  stopTimer();
683
  timestamp.textContent = '';
684
-
685
- // Ensure thoughts box is collapsed for a new exercise
686
  thoughtsBox.classList.remove('open');
687
-
688
-
689
- // Close the modal
690
  savedModal.classList.remove('active');
 
 
691
  });
692
  });
693
  </script>
 
 
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
  <head>
 
48
 
49
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
50
  .blue-button:hover { background: #2563eb; }
51
+ .blue-button:disabled { background: #9ca3af; cursor: not-allowed; }
52
+
53
 
54
  .loader {
55
  width: 48px;
 
93
  th { background-color: #f3f4f6; font-weight: 600; }
94
  .table-responsive { overflow-x: auto; }
95
 
 
96
  #saveButton {
97
  background: #3b82f6;
98
  color: white;
 
102
  }
103
  #saveButton:hover { background: #2563eb; }
104
 
 
105
  #savedModal {
106
  display: none;
107
  position: fixed;
 
117
  overflow-y: auto;
118
  }
119
 
 
120
  pre {
121
  background-color: #f8f8f8;
122
  border: 1px solid #e2e8f0;
 
140
  white-space: pre-wrap;
141
  }
142
 
 
143
  .content-text {}
144
  .content-code { padding: 0; }
145
  .content-result {
 
165
 
166
  <main id="mainContent">
167
  <form id="problemForm" class="space-y-6" novalidate>
 
168
  <div class="uploadArea p-8 text-center relative" aria-label="Zone de dépôt d'image">
169
  <input type="file" id="imageInput" name="image" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
170
  <div class="space-y-3">
 
178
  <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
179
  </div>
180
  </div>
 
181
  <div id="imagePreview" class="hidden text-center">
182
  <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
183
  </div>
184
+ <button type="submit" id="submitProblemButton" class="blue-button w-full py-3 text-white font-medium rounded-lg">
185
  Résoudre le problème
186
  </button>
187
  </form>
188
 
 
189
  <div id="loader" class="hidden mt-8 text-center">
190
  <span class="loader"></span>
191
  <p class="mt-4 text-gray-600">Analyse en cours...</p>
192
  </div>
193
 
 
194
  <section id="solution" class="hidden mt-8 space-y-6 relative">
195
  <div class="border-t pt-4">
196
  <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
 
204
  <div class="border-t pt-6">
205
  <div class="flex justify-between items-center">
206
  <h3 class="text-xl font-bold text-gray-800 mb-4">Solution</h3>
 
207
  <button id="saveButton">Sauvegarder</button>
208
  </div>
209
  <div id="answerContent" class="text-gray-700 table-responsive"></div>
 
212
  </main>
213
  </div>
214
 
 
215
  <div id="savedModal">
216
  <div id="savedModalContent" class="p-6">
217
  <header class="flex justify-between items-center border-b pb-4">
 
220
  </header>
221
  <div id="savedListContainer" class="mt-4">
222
  <ul id="savedList" class="space-y-4">
 
223
  </ul>
224
  </div>
225
  <div class="mt-6">
 
232
 
233
  <script>
234
  document.addEventListener('DOMContentLoaded', () => {
 
235
  const form = document.getElementById('problemForm');
236
  const imageInput = document.getElementById('imageInput');
237
  const loader = document.getElementById('loader');
 
249
  const savedModal = document.getElementById('savedModal');
250
  const savedList = document.getElementById('savedList');
251
  const newExercise = document.getElementById('newExercise');
252
+ const mainContent = document.getElementById('mainContent');
253
+ const submitButton = document.getElementById('submitProblemButton'); // Récupérer le bouton de soumission
254
 
255
  let startTime = null;
256
  let timerInterval = null;
 
258
  let answerBuffer = '';
259
  let currentMode = null;
260
  let updateTimeout = null;
 
261
 
262
+ // Constantes pour la gestion du délai de soumission
263
+ const SUBMISSION_COOLDOWN_MS = 3 * 60 * 1000; // 3 minutes
264
+ const LAST_SUBMISSION_TIMESTAMP_KEY = 'mariamM1LastSubmissionTimestamp';
265
+ let cooldownInterval = null;
266
+ const ORIGINAL_SUBMIT_BUTTON_TEXT = 'Résoudre le problème';
267
+
268
+ // Fonction pour activer le bouton de soumission
269
+ const enableSubmitButton = () => {
270
+ clearInterval(cooldownInterval);
271
+ submitButton.disabled = false;
272
+ submitButton.textContent = ORIGINAL_SUBMIT_BUTTON_TEXT;
273
+ };
274
+
275
+ // Fonction pour désactiver le bouton de soumission et afficher le compte à rebours
276
+ const disableSubmitButton = (remainingTimeMs) => {
277
+ if (remainingTimeMs <= 0) {
278
+ enableSubmitButton();
279
+ localStorage.removeItem(LAST_SUBMISSION_TIMESTAMP_KEY); // Nettoyer si le temps est écoulé
280
+ return;
281
+ }
282
+
283
+ submitButton.disabled = true;
284
+ clearInterval(cooldownInterval); // S'assurer qu'il n'y a pas d'intervalle précédent
285
+
286
+ let timeLeft = Math.ceil(remainingTimeMs / 1000); // en secondes
287
+
288
+ const updateButtonText = () => {
289
+ if (timeLeft <= 0) {
290
+ enableSubmitButton();
291
+ localStorage.removeItem(LAST_SUBMISSION_TIMESTAMP_KEY); // Cooldown terminé
292
+ return;
293
+ }
294
+ const minutes = Math.floor(timeLeft / 60);
295
+ const seconds = timeLeft % 60;
296
+ submitButton.textContent = `Attendre ${minutes}m ${seconds < 10 ? '0' : ''}${seconds}s`;
297
+ timeLeft--;
298
+ };
299
+
300
+ updateButtonText(); // Appel initial
301
+ cooldownInterval = setInterval(updateButtonText, 1000);
302
+ };
303
+
304
+ // Initialiser l'état du cooldown au chargement de la page
305
+ const initializeCooldownState = () => {
306
+ const lastSubmissionTime = parseInt(localStorage.getItem(LAST_SUBMISSION_TIMESTAMP_KEY), 10);
307
+ if (lastSubmissionTime) {
308
+ const now = Date.now();
309
+ const timePassed = now - lastSubmissionTime;
310
+ if (timePassed < SUBMISSION_COOLDOWN_MS) {
311
+ const remainingTime = SUBMISSION_COOLDOWN_MS - timePassed;
312
+ disableSubmitButton(remainingTime);
313
+ } else {
314
+ enableSubmitButton();
315
+ localStorage.removeItem(LAST_SUBMISSION_TIMESTAMP_KEY); // Cooldown expiré
316
+ }
317
+ } else {
318
+ enableSubmitButton();
319
+ }
320
+ };
321
+
322
+ initializeCooldownState(); // Appeler à l'initialisation
323
+
324
 
 
325
  const updateTimestamp = () => {
326
  if (startTime) {
327
  const seconds = Math.floor((Date.now() - startTime) / 1000);
 
329
  }
330
  };
331
  const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
332
+ const stopTimer = () => { clearInterval(timerInterval); startTime = null; /* Ne pas réinitialiser le timestamp ici si on veut le garder */};
333
+
334
 
 
335
  const handleFileSelect = file => {
336
  if (!file) return;
337
  const reader = new FileReader();
 
345
  thoughtsToggle.addEventListener('click', () => { thoughtsBox.classList.toggle('open'); });
346
  imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
347
 
 
348
  const dropZone = document.querySelector('.uploadArea');
349
  dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400'); });
350
  dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); });
351
  dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); handleFileSelect(e.dataTransfer.files[0]); });
352
 
 
353
  const applyHighlighting = () => {
354
  document.querySelectorAll('pre code').forEach((block) => {
355
+ hljs.highlightElement(block); // Utiliser highlightElement pour plus de contrôle si nécessaire
356
  });
357
  };
358
 
 
359
  const typesetAnswerIfReady = async () => {
360
  if (window.mathJaxReady) {
361
+ MathJax.startup.document.elements = [document.getElementById('answerContent'), document.getElementById('thoughtsContent')];
362
  await MathJax.typesetPromise();
363
  applyHighlighting();
364
  answerContent.scrollTop = answerContent.scrollHeight;
365
+ thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
366
  } else { setTimeout(typesetAnswerIfReady, 200); }
367
  };
368
 
 
381
  highlight: function(code, lang) {
382
  if (lang && hljs.getLanguage(lang)) {
383
  try {
384
+ return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value;
385
+ } catch (error) { console.error("Highlight.js error:", error); }
386
  }
387
+ return hljs.highlightAuto(code).value; // Fallback to auto-detection
388
  }
389
  });
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  form.addEventListener('submit', async e => {
392
  e.preventDefault();
393
+
394
+ const lastSubmissionTime = parseInt(localStorage.getItem(LAST_SUBMISSION_TIMESTAMP_KEY), 10);
395
+ const currentTime = Date.now();
396
+
397
+ if (lastSubmissionTime && (currentTime - lastSubmissionTime < SUBMISSION_COOLDOWN_MS)) {
398
+ const timePassed = currentTime - lastSubmissionTime;
399
+ const remainingTime = SUBMISSION_COOLDOWN_MS - timePassed;
400
+ const minutes = Math.floor(remainingTime / (1000 * 60));
401
+ const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);
402
+
403
+ Swal.fire({
404
+ icon: 'warning',
405
+ title: 'Délai d\'attente',
406
+ text: `Veuillez attendre encore ${minutes} minute(s) et ${seconds} seconde(s) avant de soumettre à nouveau.`,
407
+ });
408
+ return; // Empêcher la soumission
409
+ }
410
+
411
  const file = imageInput.files[0];
412
  if (!file) {
413
  Swal.fire({
 
418
  return;
419
  }
420
 
421
+ // Marquer le temps de cette soumission et démarrer le cooldown
422
+ localStorage.setItem(LAST_SUBMISSION_TIMESTAMP_KEY, currentTime.toString());
423
+ disableSubmitButton(SUBMISSION_COOLDOWN_MS);
424
+
425
  startTimer();
426
  loader.classList.remove('hidden');
427
  solutionSection.classList.add('hidden');
 
429
  answerContent.innerHTML = '';
430
  thoughtsBuffer = '';
431
  answerBuffer = '';
432
+ currentMode = null;
 
 
433
  thoughtsBox.classList.add('open');
434
 
435
  const formData = new FormData();
436
  formData.append('image', file);
437
 
438
  try {
 
439
  const response = await fetch('/solve', { method: 'POST', body: formData });
440
 
441
  if (!response.body) {
 
444
 
445
  const reader = response.body.getReader();
446
  const decoder = new TextDecoder();
447
+ let streamBuffer = ''; // Renommé pour éviter confusion avec thoughtsBuffer/answerBuffer
448
 
449
  const processChunk = async chunk => {
450
+ streamBuffer += decoder.decode(chunk, { stream: true });
451
+ const lines = streamBuffer.split('\n\n');
452
+ streamBuffer = lines.pop();
453
 
454
  for (const line of lines) {
455
  if (!line.startsWith('data:')) {
456
+ console.warn('Skipping non-data line:', line);
457
  continue;
458
  }
459
  try {
 
465
  solutionSection.classList.remove('hidden');
466
  }
467
 
468
+ if (data.content !== undefined) {
 
469
  if (currentMode === 'thinking') {
 
470
  thoughtsBuffer += data.content;
471
  } else if (currentMode === 'answering') {
 
472
  switch(data.type) {
473
  case 'code':
474
+ answerBuffer += "\n```" + (data.language || '') + "\n" + data.content + "\n```\n";
475
  break;
476
  case 'result':
 
477
  const formattedResult = data.content.split('\n').map(line => `> ${line}`).join('\n');
478
  answerBuffer += "\n" + formattedResult + "\n";
479
  break;
480
  case 'image':
481
+ answerBuffer += `\n![Image générée](data:image/png;base64,${data.content})\n`;
 
482
  break;
483
+ case 'text':
484
+ default:
485
  answerBuffer += data.content;
486
  break;
487
  }
488
  }
489
  }
490
+ if (data.error) { answerBuffer += `\n**Erreur:** ${data.error}\n`; }
 
 
 
 
491
  } catch (e) {
492
  console.error('Error parsing JSON data:', line.slice(5), e);
493
+ if (currentMode === 'thinking') { thoughtsBuffer += `\n[Erreur de Parsing des Données]`; }
494
+ else { answerBuffer += `\n[Erreur de Parsing des Données]`; }
 
495
  }
496
  }
497
+ scheduleUpdate();
 
498
  };
499
 
 
500
  while (true) {
501
  const { done, value } = await reader.read();
502
  if (done) {
503
+ if (streamBuffer.startsWith('data:')) { // Traiter le dernier morceau s'il existe
504
+ try {
505
+ const data = JSON.parse(streamBuffer.slice(5));
506
+ if (data.content !== undefined) {
507
+ if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
508
+ else if (currentMode === 'answering') { answerBuffer += data.content; } // Simplifié, ajuster si types nécessaires ici
509
+ }
510
+ } catch(e) { console.error("Error parsing final buffer chunk:", e); }
511
+ } else if (streamBuffer.trim() !== '') {
512
+ console.warn("Final stream buffer part was not a data line:", streamBuffer);
 
 
 
 
 
 
 
 
513
  }
514
+ scheduleUpdate();
515
+ break;
516
  }
517
  await processChunk(value);
518
  }
519
+ stopTimer();
 
520
  } catch (error) {
521
  console.error('Erreur de Fetch ou du Stream:', error);
522
  Swal.fire({
523
  icon: 'error',
524
  title: 'Erreur de connexion ou de traitement',
525
+ text: `Une erreur est survenue: ${error.message}`
526
  });
527
  loader.classList.add('hidden');
528
+ stopTimer();
529
+ solutionSection.classList.remove('hidden');
530
+ if(answerBuffer === '') {
531
  answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
532
+ } else {
533
+ answerBuffer += `\n\n<div class="text-red-500 p-2 my-2 border border-red-500 rounded">Une erreur est survenue pendant le traitement: ${error.message}</div>`;
534
+ scheduleUpdate();
535
  }
536
+ // Le cooldown continue indépendamment de l'erreur de fetch.
537
  }
538
  });
539
 
540
 
 
541
  saveButton.addEventListener('click', () => {
542
  Swal.fire({
543
  title: 'Sauvegarder la solution',
 
546
  inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
547
  showCancelButton: true,
548
  confirmButtonText: 'Sauvegarder',
549
+ cancelButtonText: 'Annuler',
550
+ inputValidator: (value) => {
551
+ if (!value) { return 'Vous devez entrer un nom !' }
552
+ }
553
  }).then((result) => {
554
  if (result.isConfirmed && result.value) {
555
  const saveName = result.value;
556
  const saveData = {
557
+ answer: answerContent.innerHTML,
558
+ thinking: thoughtsContent.innerHTML,
559
  date: new Date().toLocaleString()
560
  };
561
  let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
562
  savedExercises[saveName] = saveData;
563
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
564
+ Swal.fire('Sauvegardé !', 'Votre solution a été sauvegardée.', 'success');
 
 
 
 
 
 
565
  }
566
  });
567
  });
568
 
 
569
  const loadSavedList = () => {
570
  savedList.innerHTML = '';
571
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
 
572
  if (Object.keys(savedExercises).length === 0) {
573
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
574
  return;
575
  }
 
 
576
  const sortedEntries = Object.entries(savedExercises).sort(([,a], [,b]) => new Date(b.date) - new Date(a.date));
 
 
577
  for (const [name, data] of sortedEntries) {
578
  const li = document.createElement('li');
579
+ li.className = 'border-b pb-2 mb-2'; // Ajout de mb-2 pour espacement
580
  li.innerHTML = `
581
  <div class="flex justify-between items-center">
582
+ <button class="text-left text-blue-600 hover:underline focus:outline-none" data-save="${name}">
583
+ <span class="font-semibold">${name}</span> <br> <span class="text-gray-500 text-xs">(${data.date})</span>
584
  </button>
585
+ <button class="text-red-500 hover:text-red-700 p-1 focus:outline-none" data-delete="${name}" aria-label="Supprimer ${name}">
586
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
587
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
588
  </svg>
589
  </button>
590
  </div>
 
593
  }
594
  };
595
 
 
596
  savedList.addEventListener('click', (e) => {
597
+ const saveButtonTarget = e.target.closest('[data-save]');
598
+ const deleteButtonTarget = e.target.closest('[data-delete]');
599
+
600
+ if (saveButtonTarget) {
601
+ const saveName = saveButtonTarget.dataset.save;
602
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
603
  const data = savedExercises[saveName];
604
  if (data) {
 
605
  form.classList.add('hidden');
606
  loader.classList.add('hidden');
607
  solutionSection.classList.remove('hidden');
 
 
608
  thoughtsContent.innerHTML = data.thinking;
609
  answerContent.innerHTML = data.answer;
 
 
610
  savedModal.classList.remove('active');
 
 
 
611
  if (window.mathJaxReady) {
612
+ MathJax.startup.document.elements = [thoughtsContent, answerContent];
613
+ MathJax.typesetPromise().then(applyHighlighting).catch(err => console.error("MathJax error on load:", err));
614
+ } else { setTimeout(() => { if(window.mathJaxReady) MathJax.typesetPromise([thoughtsContent, answerContent]).then(applyHighlighting);}, 500); }
 
 
 
 
 
 
615
  thoughtsBox.classList.add('open');
616
+ thoughtsBuffer = ''; answerBuffer = ''; stopTimer();
617
+ timestamp.textContent = `Sauvegardé le: ${data.date}`;
 
 
 
 
618
  }
619
+ } else if (deleteButtonTarget) {
620
+ const deleteName = deleteButtonTarget.dataset.delete;
 
 
 
 
621
  Swal.fire({
622
  title: 'Êtes-vous sûr ?',
623
  text: "Cette sauvegarde sera définitivement supprimée.",
624
  icon: 'warning',
625
  showCancelButton: true,
626
+ confirmButtonColor: '#d33',
627
+ cancelButtonColor: '#3085d6',
628
+ confirmButtonText: 'Oui, supprimer !',
629
  cancelButtonText: 'Annuler'
630
  }).then((result) => {
631
  if (result.isConfirmed) {
632
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
633
  delete savedExercises[deleteName];
634
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
635
+ Swal.fire('Supprimé !', 'La sauvegarde a été supprimée.', 'success');
636
+ loadSavedList();
 
 
 
 
 
 
 
 
637
  }
638
  });
639
  }
640
  });
641
 
 
642
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
643
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
644
 
 
645
  newExercise.addEventListener('click', () => {
 
646
  form.reset();
647
  form.classList.remove('hidden');
648
  solutionSection.classList.add('hidden');
649
+ imagePreview.classList.add('hidden');
650
+ previewImage.src = '';
 
 
651
  thoughtsContent.innerHTML = '';
652
  answerContent.innerHTML = '';
653
  thoughtsBuffer = '';
654
  answerBuffer = '';
655
+ currentMode = null;
 
 
656
  stopTimer();
657
  timestamp.textContent = '';
 
 
658
  thoughtsBox.classList.remove('open');
 
 
 
659
  savedModal.classList.remove('active');
660
+ // Important: vérifier le cooldown pour le nouveau bouton d'exercice
661
+ initializeCooldownState();
662
  });
663
  });
664
  </script>