Spaces:
Running
Running
Update templates/maj.html
Browse files- 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');
|
|
|
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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
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.
|
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;
|
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
|
408 |
|
409 |
const processChunk = async chunk => {
|
410 |
-
|
411 |
-
const lines =
|
412 |
-
|
413 |
|
414 |
for (const line of lines) {
|
415 |
if (!line.startsWith('data:')) {
|
416 |
-
console.warn('Skipping non-data line:', line);
|
417 |
continue;
|
418 |
}
|
419 |
try {
|
@@ -425,100 +465,79 @@
|
|
425 |
solutionSection.classList.remove('hidden');
|
426 |
}
|
427 |
|
428 |
-
if (data.content !== undefined) {
|
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
|
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 |
-
|
446 |
-
answerBuffer += `\n\n`;
|
447 |
break;
|
448 |
-
case 'text':
|
449 |
-
default:
|
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 |
-
|
463 |
-
|
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 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
if (
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
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();
|
495 |
-
break;
|
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
|
507 |
});
|
508 |
loader.classList.add('hidden');
|
509 |
-
stopTimer();
|
510 |
-
solutionSection.classList.remove('hidden');
|
511 |
-
if(answerBuffer === '') {
|
512 |
answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
|
513 |
-
} else {
|
514 |
-
answerBuffer += `\n\n<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
|
515 |
-
scheduleUpdate();
|
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,
|
536 |
-
thinking: thoughtsContent.innerHTML,
|
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 |
-
|
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-
|
577 |
-
<path
|
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 |
-
|
589 |
-
|
590 |
-
|
|
|
|
|
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];
|
610 |
-
MathJax.typesetPromise().then((
|
611 |
-
|
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 |
-
|
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: '#
|
638 |
-
cancelButtonColor: '#
|
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 |
-
|
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');
|
672 |
-
previewImage.src = '';
|
673 |
-
|
674 |
-
// Clear content areas and buffers
|
675 |
thoughtsContent.innerHTML = '';
|
676 |
answerContent.innerHTML = '';
|
677 |
thoughtsBuffer = '';
|
678 |
answerBuffer = '';
|
679 |
-
currentMode = null;
|
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\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>
|