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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +235 -102
templates/index.html CHANGED
@@ -1,4 +1,3 @@
1
-
2
  <!DOCTYPE html>
3
  <html lang="fr">
4
  <head>
@@ -48,7 +47,12 @@
48
  .uploadArea:hover { border-color: #3b82f6; }
49
 
50
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
51
- .blue-button:hover { background: #2563eb; }
 
 
 
 
 
52
 
53
  .loader {
54
  width: 48px;
@@ -161,7 +165,7 @@
161
  <header class="p-6 text-center mb-8">
162
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-?</h1>
163
  <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente, avec intégration d'une calculatrice.</p>
164
- <p style="color: red;">Mode standard. Passez au premium pour de très hautes performances.</p>
165
  <div class="mt-4 flex justify-end">
166
  <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
167
  </div>
@@ -187,7 +191,8 @@
187
  <div id="imagePreview" class="hidden text-center">
188
  <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
189
  </div>
190
- <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
 
191
  Résoudre le problème
192
  </button>
193
  </form>
@@ -246,6 +251,7 @@
246
  // Récupération des éléments
247
  const form = document.getElementById('problemForm');
248
  const imageInput = document.getElementById('imageInput');
 
249
  const loader = document.getElementById('loader');
250
  const solutionSection = document.getElementById('solution');
251
  const thoughtsContent = document.getElementById('thoughtsContent');
@@ -269,11 +275,57 @@
269
  let answerBuffer = '';
270
  let currentMode = null;
271
  let updateTimeout = null;
272
- // Removed currentEndpoint variable as it's no longer needed
273
 
274
- // Gestion des onglets pour le choix du modèle (Removed as tabs are removed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
- // Mise à jour du temps écoulé
277
  const updateTimestamp = () => {
278
  if (startTime) {
279
  const seconds = Math.floor((Date.now() - startTime) / 1000);
@@ -313,17 +365,31 @@
313
  // Rendu MathJax et mise à jour de l'affichage
314
  const typesetAnswerIfReady = async () => {
315
  if (window.mathJaxReady) {
316
- MathJax.startup.document.elements = [document.getElementById('answerContent')];
 
 
317
  await MathJax.typesetPromise();
318
- applyHighlighting();
319
- answerContent.scrollTop = answerContent.scrollHeight;
 
320
  } else { setTimeout(typesetAnswerIfReady, 200); }
321
  };
322
 
323
  const updateDisplay = async () => {
 
324
  thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
325
  answerContent.innerHTML = marked.parse(answerBuffer);
 
 
326
  await typesetAnswerIfReady();
 
 
 
 
 
 
 
 
327
  updateTimeout = null;
328
  };
329
 
@@ -336,40 +402,46 @@
336
  if (lang && hljs.getLanguage(lang)) {
337
  try {
338
  return hljs.highlight(code, { language: lang }).value;
339
- } catch (error) {}
 
 
 
 
 
 
 
 
 
 
340
  }
341
- return code;
342
  }
343
  });
344
 
345
- // 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)
346
- const createContentElement = (content, type) => {
347
- const div = document.createElement('div');
348
- div.className = `content-${type}`;
349
-
350
- switch(type) {
351
- case 'text':
352
- div.innerHTML = marked.parse(content);
353
- break;
354
- case 'code':
355
- div.innerHTML = `<pre><code>${content}</code></pre>`;
356
- break;
357
- case 'result':
358
- div.innerHTML = `<div class="code-execution-result">${content}</div>`;
359
- break;
360
- case 'image':
361
- div.innerHTML = `<img src="data:image/png;base64,${content}" />`;
362
- break;
363
- default:
364
- div.innerHTML = marked.parse(content);
365
- }
366
-
367
- return div;
368
- };
369
 
370
  // Envoi de l'image pour résolution
371
  form.addEventListener('submit', async e => {
372
- e.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  const file = imageInput.files[0];
374
  if (!file) {
375
  Swal.fire({
@@ -377,15 +449,22 @@
377
  title: 'Image manquante',
378
  text: 'Veuillez sélectionner une image.'
379
  });
 
 
380
  return;
381
  }
382
 
 
 
 
 
 
383
  startTimer();
384
  loader.classList.remove('hidden');
385
  solutionSection.classList.add('hidden');
386
- thoughtsContent.innerHTML = '';
387
- answerContent.innerHTML = '';
388
- thoughtsBuffer = '';
389
  answerBuffer = '';
390
  currentMode = null; // Reset mode
391
 
@@ -399,6 +478,12 @@
399
  // Hardcode the endpoint to /solved
400
  const response = await fetch('/solved', { method: 'POST', body: formData });
401
 
 
 
 
 
 
 
402
  if (!response.body) {
403
  throw new Error('Response body is not available (e.g., not a streaming response)');
404
  }
@@ -410,11 +495,11 @@
410
  const processChunk = async chunk => {
411
  buffer += decoder.decode(chunk, { stream: true });
412
  const lines = buffer.split('\n\n');
413
- buffer = lines.pop();
414
 
415
  for (const line of lines) {
416
  if (!line.startsWith('data:')) {
417
- console.warn('Skipping non-data line:', line); // Log non-data lines
418
  continue;
419
  }
420
  try {
@@ -422,19 +507,20 @@
422
 
423
  if (data.mode) {
424
  currentMode = data.mode;
 
425
  loader.classList.add('hidden');
426
  solutionSection.classList.remove('hidden');
427
  }
428
 
 
429
  if (data.content !== undefined) { // Check for undefined to allow empty strings
430
- // Gestion différenciée selon le type de contenu
431
  if (currentMode === 'thinking') {
432
- // For thinking mode, just append text
433
  thoughtsBuffer += data.content;
434
  } else if (currentMode === 'answering') {
435
- // For answering mode, handle different types
436
  switch(data.type) {
437
  case 'code':
 
438
  answerBuffer += "\n```\n" + data.content + "\n```\n";
439
  break;
440
  case 'result':
@@ -455,18 +541,22 @@
455
  }
456
 
457
  if (data.error) {
458
- answerBuffer += `\n**Erreur:** ${data.error}\n`;
 
 
 
 
 
459
  }
460
 
461
  } catch (e) {
462
  console.error('Error parsing JSON data:', line.slice(5), e);
463
- // Optionally append an error message to the output
464
- if (currentMode === 'thinking') { thoughtsBuffer += `\n[Parsing Error]`; }
465
- else { answerBuffer += `\n[Parsing Error]`; }
466
  }
467
  }
468
-
469
- scheduleUpdate(); // Schedule display update after processing chunks
470
  };
471
 
472
  // Start processing the stream
@@ -474,47 +564,46 @@
474
  const { done, value } = await reader.read();
475
  if (done) {
476
  // Process any remaining buffer
477
- if (buffer) {
478
- try {
479
- // Attempt to parse the last bit if it looks like a data line
480
- if (buffer.startsWith('data:')) {
481
- const data = JSON.parse(buffer.slice(5));
482
- if (data.content !== undefined) {
483
- if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
484
- else if (currentMode === 'answering') { answerBuffer += data.content; }
485
- }
486
- } else {
487
- // Handle cases where the buffer is not a complete data line (e.g., partial line)
488
- console.warn('Remaining buffer is not a complete data line:', buffer);
489
- // Decide how to handle partial data - maybe discard or append as raw? Appending as raw might break markdown. Discarding is safer.
490
  }
491
- } catch (e) {
492
- console.error('Error processing final buffer:', e);
493
- }
494
- }
495
- scheduleUpdate(); // Final display update
496
  break; // Exit loop
497
  }
 
498
  await processChunk(value);
499
  }
500
 
501
- stopTimer(); // Stop timer when stream is done
502
  } catch (error) {
503
  console.error('Erreur de Fetch ou du Stream:', error);
 
 
 
 
 
 
 
504
  Swal.fire({
505
  icon: 'error',
506
  title: 'Erreur de connexion ou de traitement',
507
- text: `Une erreur est survenue lors du traitement de votre demande: ${error.message}`
508
  });
509
- loader.classList.add('hidden');
510
- stopTimer(); // Ensure timer stops on error
511
- solutionSection.classList.remove('hidden'); // Show section maybe with error message
512
- if(answerBuffer === '') { // If no output yet, put error in answer section
513
- answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
514
- } else { // If some output exists, append error
515
- answerBuffer += `\n\n<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
516
- scheduleUpdate(); // Update display with error
517
- }
518
  }
519
  });
520
 
@@ -528,18 +617,47 @@
528
  inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
529
  showCancelButton: true,
530
  confirmButtonText: 'Sauvegarder',
531
- cancelButtonText: 'Annuler'
 
 
 
 
 
532
  }).then((result) => {
533
  if (result.isConfirmed && result.value) {
534
  const saveName = result.value;
535
- const saveData = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  answer: answerContent.innerHTML, // Save the rendered HTML
537
  thinking: thoughtsContent.innerHTML, // Save the rendered HTML
538
  date: new Date().toLocaleString()
539
  };
540
- let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
541
- savedExercises[saveName] = saveData;
542
- localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
543
  Swal.fire({
544
  icon: 'success',
545
  title: 'Sauvegarde réussie',
@@ -547,9 +665,8 @@
547
  timer: 2000,
548
  showConfirmButton: false
549
  });
550
- }
551
- });
552
- });
553
 
554
  // Chargement des sauvegardes dans le modal
555
  const loadSavedList = () => {
@@ -570,10 +687,10 @@
570
  li.className = 'border-b pb-2';
571
  li.innerHTML = `
572
  <div class="flex justify-between items-center">
573
- <button class="text-left text-blue-600 hover:underline" data-save="${name}">
574
  ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
575
  </button>
576
- <button class="text-red-500 hover:text-red-700" data-delete="${name}">
577
  <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
578
  <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" />
579
  </svg>
@@ -588,7 +705,7 @@
588
  savedList.addEventListener('click', (e) => {
589
  // Chargement d'une sauvegarde
590
  if (e.target && e.target.dataset.save) {
591
- const saveName = e.target.dataset.save;
592
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
593
  const data = savedExercises[saveName];
594
  if (data) {
@@ -619,16 +736,19 @@
619
  thoughtsBox.classList.add('open');
620
 
621
  // Reset buffers and timer as this is static content
622
- thoughtsBuffer = ''; // Or load from saved data if needed for re-processing
623
- answerBuffer = ''; // Or load from saved data if needed for re-processing
624
- stopTimer();
 
625
  timestamp.textContent = data.date; // Show saved date as timestamp
 
 
626
  }
627
  }
628
 
629
  // Suppression d'une sauvegarde
630
  if (e.target && (e.target.dataset.delete || e.target.closest('[data-delete]'))) {
631
- const deleteName = e.target.dataset.delete || e.target.closest('[data-delete]').dataset.delete;
632
 
633
  Swal.fire({
634
  title: 'Êtes-vous sûr ?',
@@ -652,8 +772,10 @@
652
  );
653
 
654
  loadSavedList(); // Refresh the list in the modal
655
- // If the currently viewed solution is the one being deleted, clear it
656
- // (Optional, maybe better to just let them view it until they start a new one)
 
 
657
  }
658
  });
659
  }
@@ -663,9 +785,9 @@
663
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
664
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
665
 
666
- // Bouton présent uniquement dans le modal pour lancer un nouvel exercice
667
- newExercise.addEventListener('click', () => {
668
- // Reset form and hide solution
669
  form.reset();
670
  form.classList.remove('hidden');
671
  solutionSection.classList.add('hidden');
@@ -686,10 +808,21 @@
686
  // Ensure thoughts box is collapsed for a new exercise
687
  thoughtsBox.classList.remove('open');
688
 
 
 
689
 
690
- // Close the modal
691
  savedModal.classList.remove('active');
692
- });
 
 
 
 
 
 
 
 
 
693
  });
694
  </script>
695
  </body>
 
 
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
  <head>
 
47
  .uploadArea:hover { border-color: #3b82f6; }
48
 
49
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
50
+ .blue-button:hover:not(:disabled) { background: #2563eb; } /* Hover only when not disabled */
51
+ .blue-button:disabled {
52
+ background: #9ca3af; /* Gray background for disabled */
53
+ cursor: not-allowed;
54
+ }
55
+
56
 
57
  .loader {
58
  width: 48px;
 
165
  <header class="p-6 text-center mb-8">
166
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-?</h1>
167
  <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente, avec intégration d'une calculatrice.</p>
168
+ <p style="color: red;">Mode standard. Passez au premium pour de très hautes performances.</p>
169
  <div class="mt-4 flex justify-end">
170
  <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
171
  </div>
 
191
  <div id="imagePreview" class="hidden text-center">
192
  <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
193
  </div>
194
+ <!-- Le bouton de soumission -->
195
+ <button type="submit" id="submitButton" class="blue-button w-full py-3 text-white font-medium rounded-lg">
196
  Résoudre le problème
197
  </button>
198
  </form>
 
251
  // Récupération des éléments
252
  const form = document.getElementById('problemForm');
253
  const imageInput = document.getElementById('imageInput');
254
+ const submitButton = document.getElementById('submitButton'); // Get the submit button
255
  const loader = document.getElementById('loader');
256
  const solutionSection = document.getElementById('solution');
257
  const thoughtsContent = document.getElementById('thoughtsContent');
 
275
  let answerBuffer = '';
276
  let currentMode = null;
277
  let updateTimeout = null;
 
278
 
279
+ // --- Délai de soumission local ---
280
+ const COOLDOWN_DURATION = 3 * 60 * 1000; // 3 minutes en millisecondes
281
+ const LAST_SUBMISSION_KEY = 'lastSubmissionTime';
282
+ let cooldownTimer = null; // Pour l'intervalle de mise à jour du bouton
283
+
284
+ // Fonction pour récupérer le timestamp de la dernière soumission depuis localStorage
285
+ const getLastSubmissionTime = () => {
286
+ const timestamp = localStorage.getItem(LAST_SUBMISSION_KEY);
287
+ return timestamp ? parseInt(timestamp, 10) : null;
288
+ };
289
+
290
+ // Fonction pour enregistrer le timestamp de la soumission actuelle dans localStorage
291
+ const setLastSubmissionTime = () => {
292
+ localStorage.setItem(LAST_SUBMISSION_KEY, Date.now().toString());
293
+ };
294
+
295
+ // Fonction pour vérifier le délai et mettre à jour l'état du bouton
296
+ const updateSubmitButtonState = () => {
297
+ const lastSubmit = getLastSubmissionTime();
298
+ const now = Date.now();
299
+
300
+ if (lastSubmit && now - lastSubmit < COOLDOWN_DURATION) {
301
+ const remainingTime = COOLDOWN_DURATION - (now - lastSubmit);
302
+ const minutes = Math.floor(remainingTime / 60000);
303
+ const seconds = Math.floor((remainingTime % 60000) / 1000);
304
+ submitButton.disabled = true;
305
+ submitButton.textContent = `Prochaine soumission dans ${minutes}:${seconds.toString().padStart(2, '0')}`;
306
+
307
+ // Démarrer le timer si ce n'est pas déjà fait
308
+ if (!cooldownTimer) {
309
+ cooldownTimer = setInterval(updateSubmitButtonState, 1000);
310
+ }
311
+ } else {
312
+ // Le délai est terminé ou il n'y a pas de soumission précédente
313
+ submitButton.disabled = false;
314
+ submitButton.textContent = 'Résoudre le problème';
315
+ // Arrêter le timer s'il était en cours
316
+ if (cooldownTimer) {
317
+ clearInterval(cooldownTimer);
318
+ cooldownTimer = null;
319
+ }
320
+ }
321
+ };
322
+
323
+ // Appeler la fonction au chargement de la page pour initialiser l'état du bouton
324
+ updateSubmitButtonState();
325
+ // --- Fin Délai de soumission local ---
326
+
327
 
328
+ // Mise à jour du temps écoulé (pour l'analyse en cours)
329
  const updateTimestamp = () => {
330
  if (startTime) {
331
  const seconds = Math.floor((Date.now() - startTime) / 1000);
 
365
  // Rendu MathJax et mise à jour de l'affichage
366
  const typesetAnswerIfReady = async () => {
367
  if (window.mathJaxReady) {
368
+ // Target specific elements for MathJax typesetting if needed, or document.body
369
+ // Targeting answerContent and thoughtsContent is safer for performance
370
+ MathJax.startup.document.elements = [document.getElementById('answerContent'), document.getElementById('thoughtsContent')];
371
  await MathJax.typesetPromise();
372
+ applyHighlighting(); // Apply highlighting after MathJax
373
+ // Keep scrolling behavior only for the main answer content maybe
374
+ // answerContent.scrollTop = answerContent.scrollHeight; // Auto-scroll might be jumpy with streaming
375
  } else { setTimeout(typesetAnswerIfReady, 200); }
376
  };
377
 
378
  const updateDisplay = async () => {
379
+ // Use innerHTML = marked.parse(...) for streaming updates
380
  thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
381
  answerContent.innerHTML = marked.parse(answerBuffer);
382
+
383
+ // Trigger MathJax typesetting and highlighting *after* updating innerHTML
384
  await typesetAnswerIfReady();
385
+
386
+ // Optional: auto-scroll only the answer content if it's the active stream
387
+ if (currentMode === 'answering') {
388
+ answerContent.scrollTop = answerContent.scrollHeight;
389
+ } else if (currentMode === 'thinking') {
390
+ thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
391
+ }
392
+
393
  updateTimeout = null;
394
  };
395
 
 
402
  if (lang && hljs.getLanguage(lang)) {
403
  try {
404
  return hljs.highlight(code, { language: lang }).value;
405
+ } catch (error) {
406
+ console.error("Highlighting error:", error);
407
+ return code; // Return original code on error
408
+ }
409
+ }
410
+ // Default highlighting for unrecognised languages or no language specified
411
+ try {
412
+ return hljs.highlightAuto(code).value;
413
+ } catch (error) {
414
+ console.error("Auto highlighting error:", error);
415
+ return code; // Return original code on error
416
  }
 
417
  }
418
  });
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
  // Envoi de l'image pour résolution
422
  form.addEventListener('submit', async e => {
423
+ e.preventDefault(); // Empêcher la soumission par défaut pour gérer le délai
424
+
425
+ // --- Vérification du délai ---
426
+ const lastSubmit = getLastSubmissionTime();
427
+ const now = Date.now();
428
+
429
+ if (lastSubmit && now - lastSubmit < COOLDOWN_DURATION) {
430
+ const remainingTime = COOLDOWN_DURATION - (now - lastSubmit);
431
+ const minutes = Math.floor(remainingTime / 60000);
432
+ const seconds = Math.floor((remainingTime % 60000) / 1000);
433
+ Swal.fire({
434
+ icon: 'warning',
435
+ title: 'Veuillez patienter',
436
+ text: `Vous devez attendre ${minutes} minute(s) et ${seconds.toString().padStart(2, '0')} seconde(s) avant de soumettre à nouveau.`
437
+ });
438
+ // Mettre à jour l'état du bouton immédiatement après le message d'erreur
439
+ updateSubmitButtonState();
440
+ return; // Arrêter le processus de soumission
441
+ }
442
+ // --- Fin Vérification du délai ---
443
+
444
+
445
  const file = imageInput.files[0];
446
  if (!file) {
447
  Swal.fire({
 
449
  title: 'Image manquante',
450
  text: 'Veuillez sélectionner une image.'
451
  });
452
+ // Re-vérifier et potentiellement réactiver le bouton si aucune image n'est sélectionnée
453
+ updateSubmitButtonState();
454
  return;
455
  }
456
 
457
+ // --- Le délai est passé ou n'existait pas, on procède ---
458
+ setLastSubmissionTime(); // Enregistrer le nouveau timestamp *avant* de commencer
459
+ updateSubmitButtonState(); // Désactiver et mettre à jour le bouton
460
+
461
+
462
  startTimer();
463
  loader.classList.remove('hidden');
464
  solutionSection.classList.add('hidden');
465
+ thoughtsContent.innerHTML = ''; // Vider le contenu précédent
466
+ answerContent.innerHTML = ''; // Vider le contenu précédent
467
+ thoughtsBuffer = ''; // Vider les buffers
468
  answerBuffer = '';
469
  currentMode = null; // Reset mode
470
 
 
478
  // Hardcode the endpoint to /solved
479
  const response = await fetch('/solved', { method: 'POST', body: formData });
480
 
481
+ if (!response.ok) {
482
+ // Handle HTTP errors
483
+ const errorText = await response.text();
484
+ throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
485
+ }
486
+
487
  if (!response.body) {
488
  throw new Error('Response body is not available (e.g., not a streaming response)');
489
  }
 
495
  const processChunk = async chunk => {
496
  buffer += decoder.decode(chunk, { stream: true });
497
  const lines = buffer.split('\n\n');
498
+ buffer = lines.pop(); // Keep the last potentially incomplete line in buffer
499
 
500
  for (const line of lines) {
501
  if (!line.startsWith('data:')) {
502
+ // console.warn('Skipping non-data line:', line); // Log non-data lines
503
  continue;
504
  }
505
  try {
 
507
 
508
  if (data.mode) {
509
  currentMode = data.mode;
510
+ // Hide loader and show solution section once streaming starts
511
  loader.classList.add('hidden');
512
  solutionSection.classList.remove('hidden');
513
  }
514
 
515
+ // Process content based on currentMode and data type
516
  if (data.content !== undefined) { // Check for undefined to allow empty strings
 
517
  if (currentMode === 'thinking') {
 
518
  thoughtsBuffer += data.content;
519
  } else if (currentMode === 'answering') {
520
+ // Handle different types within answering mode
521
  switch(data.type) {
522
  case 'code':
523
+ // Use markdown code block syntax
524
  answerBuffer += "\n```\n" + data.content + "\n```\n";
525
  break;
526
  case 'result':
 
541
  }
542
 
543
  if (data.error) {
544
+ // Append error to the relevant buffer or main answer buffer
545
+ if (currentMode === 'thinking') {
546
+ thoughtsBuffer += `\n**Erreur pendant la réflexion:** ${data.error}\n`;
547
+ } else { // Assume error relates to the answer process
548
+ answerBuffer += `\n**Erreur:** ${data.error}\n`;
549
+ }
550
  }
551
 
552
  } catch (e) {
553
  console.error('Error parsing JSON data:', line.slice(5), e);
554
+ // Append a parsing error message to the output
555
+ if (currentMode === 'thinking') { thoughtsBuffer += `\n[Erreur de traitement du flux]`; }
556
+ else { answerBuffer += `\n[Erreur de traitement du flux]`; }
557
  }
558
  }
559
+ scheduleUpdate(); // Schedule display update after processing a batch of lines
 
560
  };
561
 
562
  // Start processing the stream
 
564
  const { done, value } = await reader.read();
565
  if (done) {
566
  // Process any remaining buffer
567
+ if (buffer.length > 0) { // Process any data left in the buffer
568
+ // Even if it's not a full line, decode and append what's left
569
+ // Note: This might result in incomplete markdown being displayed
570
+ if (currentMode === 'thinking') {
571
+ thoughtsBuffer += decoder.decode(buffer, { stream: true });
572
+ } else if (currentMode === 'answering') {
573
+ answerBuffer += decoder.decode(buffer, { stream: true });
 
 
 
 
 
 
574
  }
575
+ buffer = ''; // Clear buffer after processing
576
+ }
577
+ scheduleUpdate(); // Final display update to render last buffer content
 
 
578
  break; // Exit loop
579
  }
580
+ // Process the received chunk
581
  await processChunk(value);
582
  }
583
 
 
584
  } catch (error) {
585
  console.error('Erreur de Fetch ou du Stream:', error);
586
+ // Append a user-friendly error message to the answer area if nothing is there, or append to existing content
587
+ if(answerContent.innerHTML === '' && answerBuffer === '') { // If solution area is empty
588
+ answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue lors du traitement de votre demande: ${error.message}</div>`;
589
+ } else { // If some output exists, append the error
590
+ answerBuffer += `\n\n<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
591
+ scheduleUpdate(); // Update display with the appended error
592
+ }
593
  Swal.fire({
594
  icon: 'error',
595
  title: 'Erreur de connexion ou de traitement',
596
+ text: `Une erreur est survenue lors du traitement de votre demande. Détails: ${error.message}`
597
  });
598
+ } finally {
599
+ // Code qui s'exécute après try/catch, qu'il y ait eu une erreur ou non
600
+ stopTimer(); // Arrêter le timer de l'analyse
601
+ loader.classList.add('hidden'); // Cacher le loader
602
+ solutionSection.classList.remove('hidden'); // S'assurer que la section solution est visible
603
+
604
+ // --- Mise à jour finale de l'état du bouton ---
605
+ updateSubmitButtonState(); // Vérifier si le délai est terminé et mettre à jour le bouton
606
+ // --- Fin Mise à jour finale ---
607
  }
608
  });
609
 
 
617
  inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
618
  showCancelButton: true,
619
  confirmButtonText: 'Sauvegarder',
620
+ cancelButtonText: 'Annuler',
621
+ inputValidator: (value) => {
622
+ if (!value) {
623
+ return 'Veuillez donner un nom à votre sauvegarde !';
624
+ }
625
+ }
626
  }).then((result) => {
627
  if (result.isConfirmed && result.value) {
628
  const saveName = result.value;
629
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
630
+
631
+ if (savedExercises[saveName]) {
632
+ // Ask if user wants to overwrite
633
+ Swal.fire({
634
+ title: `Sauvegarde "${saveName}" existe déjà. Écraser ?`,
635
+ icon: 'warning',
636
+ showCancelButton: true,
637
+ confirmButtonColor: '#3085d6',
638
+ cancelButtonColor: '#d33',
639
+ confirmButtonText: 'Oui, écraser',
640
+ cancelButtonText: 'Annuler'
641
+ }).then((overwriteResult) => {
642
+ if (overwriteResult.isConfirmed) {
643
+ saveCurrentSolution(saveName, savedExercises);
644
+ }
645
+ });
646
+ } else {
647
+ saveCurrentSolution(saveName, savedExercises);
648
+ }
649
+ }
650
+ });
651
+ });
652
+
653
+ const saveCurrentSolution = (saveName, existingSaves) => {
654
+ const saveData = {
655
  answer: answerContent.innerHTML, // Save the rendered HTML
656
  thinking: thoughtsContent.innerHTML, // Save the rendered HTML
657
  date: new Date().toLocaleString()
658
  };
659
+ existingSaves[saveName] = saveData;
660
+ localStorage.setItem('savedExercises', JSON.stringify(existingSaves));
 
661
  Swal.fire({
662
  icon: 'success',
663
  title: 'Sauvegarde réussie',
 
665
  timer: 2000,
666
  showConfirmButton: false
667
  });
668
+ };
669
+
 
670
 
671
  // Chargement des sauvegardes dans le modal
672
  const loadSavedList = () => {
 
687
  li.className = 'border-b pb-2';
688
  li.innerHTML = `
689
  <div class="flex justify-between items-center">
690
+ <button class="text-left text-blue-600 hover:underline" data-save="${encodeURIComponent(name)}">
691
  ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
692
  </button>
693
+ <button class="text-red-500 hover:text-red-700" data-delete="${encodeURIComponent(name)}">
694
  <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
695
  <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" />
696
  </svg>
 
705
  savedList.addEventListener('click', (e) => {
706
  // Chargement d'une sauvegarde
707
  if (e.target && e.target.dataset.save) {
708
+ const saveName = decodeURIComponent(e.target.dataset.save);
709
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
710
  const data = savedExercises[saveName];
711
  if (data) {
 
736
  thoughtsBox.classList.add('open');
737
 
738
  // Reset buffers and timer as this is static content
739
+ thoughtsBuffer = ''; // Buffers are for streaming, not needed for static load
740
+ answerBuffer = ''; // Buffers are for streaming, not needed for static load
741
+ currentMode = null; // Not streaming
742
+ stopTimer(); // Stop the live timer
743
  timestamp.textContent = data.date; // Show saved date as timestamp
744
+ submitButton.disabled = true; // Disable submit button while viewing a save
745
+ submitButton.textContent = `Vue de la sauvegarde: ${saveName}`;
746
  }
747
  }
748
 
749
  // Suppression d'une sauvegarde
750
  if (e.target && (e.target.dataset.delete || e.target.closest('[data-delete]'))) {
751
+ const deleteName = decodeURIComponent(e.target.dataset.delete || e.target.closest('[data-delete]').dataset.delete);
752
 
753
  Swal.fire({
754
  title: 'Êtes-vous sûr ?',
 
772
  );
773
 
774
  loadSavedList(); // Refresh the list in the modal
775
+ // If the deleted save was being viewed, reset the view
776
+ if (submitButton.textContent.includes(`Vue de la sauvegarde: ${deleteName}`)) {
777
+ resetToNewExerciseState();
778
+ }
779
  }
780
  });
781
  }
 
785
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
786
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
787
 
788
+ // Fonction pour réinitialiser l'état pour un nouvel exercice
789
+ const resetToNewExerciseState = () => {
790
+ // Reset form and hide solution
791
  form.reset();
792
  form.classList.remove('hidden');
793
  solutionSection.classList.add('hidden');
 
808
  // Ensure thoughts box is collapsed for a new exercise
809
  thoughtsBox.classList.remove('open');
810
 
811
+ // Update button state based on cooldown
812
+ updateSubmitButtonState(); // This will enable/disable based on the 3-min rule
813
 
814
+ // Close the modal if open
815
  savedModal.classList.remove('active');
816
+ };
817
+
818
+
819
+ // Bouton présent uniquement dans le modal pour lancer un nouvel exercice
820
+ newExercise.addEventListener('click', resetToNewExerciseState);
821
+
822
+
823
+ // Initial check for cooldown on page load
824
+ updateSubmitButtonState();
825
+
826
  });
827
  </script>
828
  </body>