Spaces:
Running
Running
Update templates/index.html
Browse files- 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 |
-
|
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 |
-
|
|
|
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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
317 |
await MathJax.typesetPromise();
|
318 |
-
applyHighlighting();
|
319 |
-
|
|
|
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 |
-
//
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
459 |
}
|
460 |
|
461 |
} catch (e) {
|
462 |
console.error('Error parsing JSON data:', line.slice(5), e);
|
463 |
-
//
|
464 |
-
if (currentMode === 'thinking') { thoughtsBuffer += `\n[
|
465 |
-
else { answerBuffer += `\n[
|
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 |
-
|
478 |
-
|
479 |
-
//
|
480 |
-
if (
|
481 |
-
|
482 |
-
|
483 |
-
|
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 |
-
|
492 |
-
|
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 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
536 |
answer: answerContent.innerHTML, // Save the rendered HTML
|
537 |
thinking: thoughtsContent.innerHTML, // Save the rendered HTML
|
538 |
date: new Date().toLocaleString()
|
539 |
};
|
540 |
-
|
541 |
-
savedExercises
|
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 = ''; //
|
623 |
-
answerBuffer = ''; //
|
624 |
-
|
|
|
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
|
656 |
-
|
|
|
|
|
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 |
-
//
|
667 |
-
|
668 |
-
|
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 |
-
|
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>
|