Docfile commited on
Commit
3a1bb56
·
verified ·
1 Parent(s): 7c947e5

Update templates/maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +480 -366
templates/maj.html CHANGED
@@ -3,12 +3,18 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Mariam M-0 | Solution Mathématique</title>
7
  <!-- Tailwind CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
 
9
  <!-- SweetAlert2 -->
10
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
11
- <!-- MathJax Configuration -->
 
 
 
 
 
12
  <script>
13
  window.MathJax = {
14
  tex: {
@@ -28,52 +34,130 @@
28
  </script>
29
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
30
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
 
31
  <style>
32
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
33
  body { font-family: 'Space Grotesk', sans-serif; }
34
- .uploadArea { background: #f3f4f6; border: 2px dashed #d1d5db; transition: border-color 0.2s ease; }
 
 
 
 
 
35
  .uploadArea:hover { border-color: #3b82f6; }
 
36
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
37
  .blue-button:hover { background: #2563eb; }
38
  .blue-button:disabled { background: #9ca3af; cursor: not-allowed; }
 
 
39
  .loader {
40
- width: 48px; height: 48px; border: 3px solid #3b82f6; border-bottom-color: transparent;
41
- border-radius: 50%; display: inline-block; animation: rotation 1s linear infinite;
 
 
 
 
 
42
  }
43
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
44
- .thought-box { transition: max-height 0.3s ease-out; max-height: 0; overflow: hidden; }
45
- .thought-box.open { max-height: 500px; /* Ajustez au besoin */ }
 
 
 
 
 
 
46
  #thoughtsContent, #answerContent {
47
- max-height: 500px; /* Ajustez au besoin */
48
- overflow-y: auto; scroll-behavior: smooth; white-space: pre-wrap;
 
 
49
  }
 
50
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
 
51
  .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
52
- table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
53
- th, td { border: 1px solid #d1d5db; padding: 0.5rem; text-align: left; }
 
 
 
 
 
 
 
 
 
54
  th { background-color: #f3f4f6; font-weight: 600; }
55
  .table-responsive { overflow-x: auto; }
 
56
  #saveButton {
57
- background: #3b82f6; color: white; padding: 0.5rem 1rem;
58
- border-radius: 0.375rem; transition: background-color 0.2s ease;
 
 
 
59
  }
60
  #saveButton:hover { background: #2563eb; }
 
61
  #savedModal {
62
- display: none; position: fixed; inset: 0;
63
- background: rgba(0,0,0,0.5); z-index: 50;
 
 
 
64
  }
65
  #savedModal.active { display: block; }
66
- #savedModalContent { background: #fff; width: 100%; height: 100%; overflow-y: auto; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  </style>
68
  </head>
69
- <body class="p-4 bg-gray-100">
70
- <div class="max-w-4xl mx-auto bg-white shadow-lg rounded-lg p-6">
71
- <header class="p-6 text-center mb-8 border-b">
72
- <h1 class="text-4xl font-bold text-blue-600">Mariam M-0</h1>
73
- <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
74
- <p class="performance-warning text-sm text-yellow-600 mt-2">
75
- Vous utilisez actuellement les modèles/performances moyens. Accédez à des performances supérieures avec un abonnement premium !
76
- </p>
77
  <div class="mt-4 flex justify-end">
78
  <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
79
  </div>
@@ -81,8 +165,8 @@
81
 
82
  <main id="mainContent">
83
  <form id="problemForm" class="space-y-6" novalidate>
84
- <div class="uploadArea p-8 text-center relative rounded-md" aria-label="Zone de dépôt d'image">
85
- <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
86
  <div class="space-y-3">
87
  <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
88
  <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
@@ -95,9 +179,9 @@
95
  </div>
96
  </div>
97
  <div id="imagePreview" class="hidden text-center">
98
- <img id="previewImage" class="preview-image mx-auto rounded-md" alt="Prévisualisation de l'image">
99
  </div>
100
- <button type="submit" id="submitButton" class="blue-button w-full py-3 text-white font-medium rounded-lg">
101
  Résoudre le problème
102
  </button>
103
  </form>
@@ -107,39 +191,40 @@
107
  <p class="mt-4 text-gray-600">Analyse en cours...</p>
108
  </div>
109
 
110
- <section id="solutionSection" class="hidden mt-8 space-y-6 relative">
111
  <div class="border-t pt-4">
112
- <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2 hover:bg-gray-100 rounded">
113
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
114
  <span id="timestamp" class="timestamp"></span>
115
  </button>
116
- <div id="thoughtsBox" class="thought-box bg-gray-50 rounded-md">
117
  <div id="thoughtsContent" class="p-4 text-gray-600"></div>
118
  </div>
119
  </div>
120
  <div class="border-t pt-6">
121
- <div class="flex justify-between items-center mb-4">
122
- <h3 class="text-xl font-bold text-gray-800">Solution</h3>
123
- <button id="saveButton" class="blue-button">Sauvegarder</button>
124
  </div>
125
- <div id="answerContent" class="text-gray-700 table-responsive p-4 border rounded-md"></div>
126
  </div>
127
  </section>
128
  </main>
129
  </div>
130
 
131
- <div id="savedModal" class="fixed inset-0 bg-gray-900 bg-opacity-50 z-50">
132
- <div id="savedModalContent" class="p-6 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl w-11/12 md:w-2/3 lg:w-1/2 max-h-screen">
133
  <header class="flex justify-between items-center border-b pb-4">
134
- <h2 class="text-2xl font-bold text-gray-800">Sauvegardes</h2>
135
- <button id="closeSaved" class="text-3xl text-gray-600 hover:text-gray-800">×</button>
136
  </header>
137
- <div id="savedListContainer" class="mt-4 max-h-96 overflow-y-auto">
138
- <ul id="savedList" class="space-y-4"></ul>
 
139
  </div>
140
- <div class="mt-6 flex justify-end">
141
- <button id="newExerciseFromModal" class="blue-button px-4 py-2 text-white font-medium rounded-lg">
142
- Nouvel Exercice
143
  </button>
144
  </div>
145
  </div>
@@ -147,405 +232,434 @@
147
 
148
  <script>
149
  document.addEventListener('DOMContentLoaded', () => {
150
- // Configuration
151
- const COOLDOWN_DURATION_MS = 3 * 60 * 1000; // 3 minutes
152
- const LAST_SUBMISSION_TIME_KEY = 'mariamM0_lastSubmissionTime';
153
- const SUBMIT_BUTTON_ORIGINAL_TEXT = 'Résoudre le problème';
154
-
155
- // DOM Elements
156
- const elements = {
157
- form: document.getElementById('problemForm'),
158
- imageInput: document.getElementById('imageInput'),
159
- submitButton: document.getElementById('submitButton'),
160
- loader: document.getElementById('loader'),
161
- solutionSection: document.getElementById('solutionSection'),
162
- thoughtsContent: document.getElementById('thoughtsContent'),
163
- answerContent: document.getElementById('answerContent'),
164
- thoughtsToggle: document.getElementById('thoughtsToggle'),
165
- thoughtsBox: document.getElementById('thoughtsBox'),
166
- imagePreview: document.getElementById('imagePreview'),
167
- previewImage: document.getElementById('previewImage'),
168
- timestamp: document.getElementById('timestamp'),
169
- saveButton: document.getElementById('saveButton'),
170
- openSaved: document.getElementById('openSaved'),
171
- closeSaved: document.getElementById('closeSaved'),
172
- savedModal: document.getElementById('savedModal'),
173
- savedList: document.getElementById('savedList'),
174
- newExerciseFromModal: document.getElementById('newExerciseFromModal'),
175
- dropZone: document.querySelector('.uploadArea')
 
 
 
 
 
 
 
 
 
 
 
 
176
  };
177
 
178
- // Application State
179
- const state = {
180
- startTime: null,
181
- timerInterval: null,
182
- cooldownTimerInterval: null,
183
- thoughtsBuffer: '',
184
- answerBuffer: '',
185
- currentMode: null, // 'thinking' or 'answering'
186
- updateTimeout: null,
187
- selectedFile: null
188
- };
189
 
190
- marked.setOptions({ gfm: true, breaks: true });
 
191
 
192
- // --- Helper Functions ---
193
- const formatTime = (totalSeconds) => {
194
- const minutes = Math.floor(totalSeconds / 60);
195
- const seconds = totalSeconds % 60;
196
- return `${minutes}m ${seconds < 10 ? '0' : ''}${seconds}s`;
197
- };
198
 
199
- const updateTimestampDisplay = () => {
200
- if (state.startTime) {
201
- const seconds = Math.floor((Date.now() - state.startTime) / 1000);
202
- elements.timestamp.textContent = `${seconds}s`;
203
- }
204
- };
 
 
 
 
 
205
 
206
- const startSolutionTimer = () => {
207
- state.startTime = Date.now();
208
- if (state.timerInterval) clearInterval(state.timerInterval);
209
- state.timerInterval = setInterval(updateTimestampDisplay, 1000);
210
- updateTimestampDisplay();
211
  };
212
 
213
- const stopSolutionTimer = () => {
214
- clearInterval(state.timerInterval);
215
- state.timerInterval = null;
216
- // Ne pas réinitialiser startTime ici pour conserver le temps final affiché
217
- };
218
-
219
- const resetSolutionTimer = () => {
220
- stopSolutionTimer();
221
- state.startTime = null;
222
- elements.timestamp.textContent = '';
 
 
 
 
 
 
223
  };
224
 
225
- const resetUIForNewProblem = () => {
226
- elements.form.reset();
227
- elements.imageInput.value = ''; // Important pour permettre la re-sélection du même fichier
228
- state.selectedFile = null;
229
- elements.imagePreview.classList.add('hidden');
230
- elements.previewImage.src = '#'; // Clear previous image
231
- elements.solutionSection.classList.add('hidden');
232
- elements.loader.classList.add('hidden');
233
- elements.thoughtsContent.innerHTML = '';
234
- elements.answerContent.innerHTML = '';
235
- state.thoughtsBuffer = '';
236
- state.answerBuffer = '';
237
- state.currentMode = null;
238
- elements.thoughtsBox.classList.remove('open'); // Fermer par défaut
239
- elements.form.classList.remove('hidden');
240
- resetSolutionTimer();
241
- updateSubmitButtonState(); // Met à jour l'état du bouton (cooldown)
242
- };
243
 
244
- // --- Cooldown Logic ---
245
- const getLastSubmissionTime = () => parseInt(localStorage.getItem(LAST_SUBMISSION_TIME_KEY) || '0');
246
-
247
- const setLastSubmissionTime = () => localStorage.setItem(LAST_SUBMISSION_TIME_KEY, Date.now().toString());
248
-
249
- const updateSubmitButtonState = () => {
250
- const lastSubmission = getLastSubmissionTime();
251
- const now = Date.now();
252
- const timeSinceLastSubmission = now - lastSubmission;
253
-
254
- if (state.cooldownTimerInterval) clearInterval(state.cooldownTimerInterval);
255
-
256
- if (timeSinceLastSubmission < COOLDOWN_DURATION_MS) {
257
- elements.submitButton.disabled = true;
258
- let remainingTimeMs = COOLDOWN_DURATION_MS - timeSinceLastSubmission;
259
-
260
- const updateButtonText = () => {
261
- const remainingSeconds = Math.ceil(remainingTimeMs / 1000);
262
- elements.submitButton.textContent = `Attendre ${formatTime(remainingSeconds)}`;
263
- remainingTimeMs -= 1000;
264
- if (remainingTimeMs < 0) {
265
- clearInterval(state.cooldownTimerInterval);
266
- state.cooldownTimerInterval = null;
267
- elements.submitButton.disabled = false;
268
- elements.submitButton.textContent = SUBMIT_BUTTON_ORIGINAL_TEXT;
269
- }
270
- };
271
- updateButtonText(); // Appel immédiat
272
- state.cooldownTimerInterval = setInterval(updateButtonText, 1000);
273
- } else {
274
- elements.submitButton.disabled = false;
275
- elements.submitButton.textContent = SUBMIT_BUTTON_ORIGINAL_TEXT;
276
  }
277
  };
 
 
278
 
279
- // --- File Handling ---
280
- const handleFileSelect = (file) => {
281
- if (!file || !file.type.startsWith('image/')) {
282
- Swal.fire('Fichier Invalide', 'Veuillez sélectionner un fichier image.', 'error');
283
- elements.imageInput.value = ''; // Reset file input
284
- state.selectedFile = null;
285
- elements.imagePreview.classList.add('hidden');
286
- return;
287
- }
288
- state.selectedFile = file;
289
  const reader = new FileReader();
290
  reader.onload = e => {
291
- elements.previewImage.src = e.target.result;
292
- elements.imagePreview.classList.remove('hidden');
293
  };
294
  reader.readAsDataURL(file);
295
  };
296
 
297
- // --- MathJax & Display Update ---
298
- const typesetMathJaxContent = async (contentElement) => {
299
- if (window.mathJaxReady && contentElement) {
300
- MathJax.startup.document.elements = [contentElement];
301
- try {
302
- await MathJax.typesetPromise();
303
- contentElement.scrollTop = contentElement.scrollHeight;
304
- } catch(err) {
305
- console.error("MathJax typesetting error:", err);
306
- }
307
- } else if (contentElement) {
308
- setTimeout(() => typesetMathJaxContent(contentElement), 200);
309
- }
 
 
 
 
 
 
 
 
 
310
  };
311
 
312
- const scheduleDisplayUpdate = () => {
313
- if (state.updateTimeout) clearTimeout(state.updateTimeout);
314
- state.updateTimeout = setTimeout(async () => {
315
- if (elements.thoughtsContent) elements.thoughtsContent.innerHTML = marked.parse(state.thoughtsBuffer);
316
- if (elements.answerContent) elements.answerContent.innerHTML = marked.parse(state.answerBuffer);
317
-
318
- // Typeset MathJax for both, but only if the section is visible or relevant
319
- if (state.thoughtsBuffer) await typesetMathJaxContent(elements.thoughtsContent);
320
- if (state.answerBuffer) await typesetMathJaxContent(elements.answerContent);
321
-
322
- state.updateTimeout = null;
323
- }, 150); // Légèrement réduit pour une sensation plus réactive
324
  };
325
 
326
- // --- Event Listeners ---
327
- elements.thoughtsToggle.addEventListener('click', () => elements.thoughtsBox.classList.toggle('open'));
328
- elements.imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
329
 
330
- elements.dropZone.addEventListener('dragover', e => { e.preventDefault(); elements.dropZone.classList.add('border-blue-400'); });
331
- elements.dropZone.addEventListener('dragleave', e => { e.preventDefault(); elements.dropZone.classList.remove('border-blue-400'); });
332
- elements.dropZone.addEventListener('drop', e => {
333
- e.preventDefault();
334
- elements.dropZone.classList.remove('border-blue-400');
335
- if (e.dataTransfer.files && e.dataTransfer.files[0]) {
336
- handleFileSelect(e.dataTransfer.files[0]);
337
- elements.imageInput.files = e.dataTransfer.files; // Ensure input has the file for form submission
 
 
338
  }
339
  });
340
 
341
- elements.form.addEventListener('submit', async e => {
342
  e.preventDefault();
343
- if (!state.selectedFile) {
344
- Swal.fire('Aucune Image', 'Veuillez sélectionner une image.', 'warning');
345
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  }
347
 
348
- const lastSubmission = getLastSubmissionTime();
349
- const now = Date.now();
350
- if (now - lastSubmission < COOLDOWN_DURATION_MS) {
351
- const remainingSeconds = Math.ceil((COOLDOWN_DURATION_MS - (now - lastSubmission)) / 1000);
352
- Swal.fire('Cooldown Actif', `Vous devez attendre ${formatTime(remainingSeconds)} avant de soumettre à nouveau.`, 'info');
 
 
353
  return;
354
  }
355
 
356
- setLastSubmissionTime();
357
- updateSubmitButtonState(); // Démarre le cooldown visuel immédiatement
 
358
 
359
- startSolutionTimer();
360
- elements.loader.classList.remove('hidden');
361
- elements.solutionSection.classList.add('hidden');
362
- elements.thoughtsContent.innerHTML = '';
363
- elements.answerContent.innerHTML = '';
364
- state.thoughtsBuffer = '';
365
- state.answerBuffer = '';
366
- state.currentMode = null;
367
- elements.thoughtsBox.classList.add('open'); // Ouvrir les réflexions par défaut
368
 
369
  const formData = new FormData();
370
- formData.append('image', state.selectedFile);
371
 
372
  try {
373
- const response = await fetch('/solved', { method: 'POST', body: formData });
374
- if (!response.ok) {
375
- throw new Error(`HTTP error! status: ${response.status}`);
 
376
  }
 
377
  const reader = response.body.getReader();
378
  const decoder = new TextDecoder();
379
- let streamBuffer = '';
380
 
381
- // eslint-disable-next-line no-constant-condition
382
- while (true) {
383
- const { done, value } = await reader.read();
384
- if (done) {
385
- if (streamBuffer.startsWith('data:')) { // Process any remaining data in buffer
386
- try {
387
- const data = JSON.parse(streamBuffer.slice(5));
388
- if (data.content) {
389
- if (state.currentMode === 'thinking') state.thoughtsBuffer += data.content;
390
- else if (state.currentMode === 'answering') state.answerBuffer += data.content;
391
- }
392
- } catch (parseError) { console.warn("Error parsing final chunk:", parseError, "Buffer:", streamBuffer); }
393
  }
394
- scheduleDisplayUpdate(); // Final update
395
- break;
396
- }
397
 
398
- streamBuffer += decoder.decode(value, { stream: true });
399
- const parts = streamBuffer.split('\n\n');
400
- streamBuffer = parts.pop(); // Keep the last, possibly incomplete, part
 
 
401
 
402
- for (const part of parts) {
403
- if (!part.startsWith('data:')) continue;
404
- try {
405
- const jsonData = part.slice(5);
406
- const data = JSON.parse(jsonData);
407
- if (data.mode) {
408
- state.currentMode = data.mode;
409
- if (!elements.loader.classList.contains('hidden')) { // Hide loader on first mode message
410
- elements.loader.classList.add('hidden');
411
- elements.solutionSection.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
412
  }
 
 
 
 
 
413
  }
414
- if (data.content) {
415
- if (state.currentMode === 'thinking') state.thoughtsBuffer += data.content;
416
- else if (state.currentMode === 'answering') state.answerBuffer += data.content;
417
- }
418
- } catch (parseError) {
419
- console.warn("Error parsing stream part:", parseError, "Part:", part);
 
 
 
 
 
 
 
 
 
 
 
420
  }
 
 
421
  }
422
- scheduleDisplayUpdate();
423
  }
 
424
  } catch (error) {
425
- console.error('Erreur de soumission:', error);
426
- Swal.fire('Erreur', `Une erreur est survenue lors de la résolution: ${error.message}`, 'error');
427
- elements.loader.classList.add('hidden');
428
- } finally {
429
- stopSolutionTimer(); // Arrête le timer de la solution, mais n'efface pas le temps affiché
430
- // Le cooldown du bouton est déjà géré par updateSubmitButtonState()
 
 
 
 
 
 
 
 
 
 
431
  }
432
  });
433
 
434
- // --- Saved Solutions Logic ---
435
- elements.saveButton.addEventListener('click', async () => {
436
- const { value: saveName } = await Swal.fire({
437
- title: 'Nom de la sauvegarde',
438
- input: 'text',
439
- inputPlaceholder: 'Ex: Exercice Maths Ch.3',
440
- showCancelButton: true,
441
- confirmButtonText: 'Sauvegarder',
442
- cancelButtonText: 'Annuler',
443
- inputValidator: (value) => {
444
- if (!value) return 'Vous devez entrer un nom !';
445
- const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
446
- if (savedExercises[value]) return 'Ce nom existe déjà. Choisissez-en un autre.';
447
- }
448
- });
449
 
450
- if (saveName) {
451
- const saveData = {
452
- answer: elements.answerContent.innerHTML,
453
- thinking: elements.thoughtsContent.innerHTML,
454
- date: new Date().toLocaleString('fr-FR')
455
- };
456
- let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
457
- savedExercises[saveName] = saveData;
458
- localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
459
- Swal.fire('Sauvegardé!', 'Votre solution a été sauvegardée.', 'success');
460
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  });
462
 
463
  const loadSavedList = () => {
464
- elements.savedList.innerHTML = ''; // Clear previous list
465
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
466
  if (Object.keys(savedExercises).length === 0) {
467
- elements.savedList.innerHTML = '<li class="text-gray-500">Aucune sauvegarde pour le moment.</li>';
468
- return;
469
  }
470
- for (const [name, data] of Object.entries(savedExercises).sort((a,b) => new Date(b[1].date) - new Date(a[1].date))) { // Sort by date desc
 
471
  const li = document.createElement('li');
472
- li.className = 'flex justify-between items-center p-2 hover:bg-gray-100 rounded';
473
  li.innerHTML = `
474
- <button class="text-left text-blue-600 hover:underline focus:outline-none" data-save-name="${name}">
475
- ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
476
- </button>
477
- <button class="text-red-500 hover:text-red-700 text-xs p-1 focus:outline-none" data-delete-name="${name}" aria-label="Supprimer ${name}">
478
- Supprimer
479
- </button>
 
 
 
 
480
  `;
481
- elements.savedList.appendChild(li);
482
  }
483
  };
484
 
485
- elements.savedList.addEventListener('click', (e) => {
486
- const target = e.target.closest('button');
487
- if (!target) return;
488
 
489
- const saveName = target.dataset.saveName;
490
- const deleteName = target.dataset.deleteName;
491
-
492
- if (saveName) {
493
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
494
  const data = savedExercises[saveName];
495
  if (data) {
496
- resetUIForNewProblem(); // Nettoie l'UI avant de charger
497
- elements.form.classList.add('hidden'); // Cache le formulaire car on affiche une solution
498
- elements.loader.classList.add('hidden');
499
- elements.solutionSection.classList.remove('hidden');
500
- elements.thoughtsContent.innerHTML = data.thinking;
501
- elements.answerContent.innerHTML = data.answer;
502
- state.thoughtsBuffer = ''; // Pas besoin de buffer pour les sauvegardes chargées
503
- state.answerBuffer = '';
504
- typesetMathJaxContent(elements.thoughtsContent).then(() => typesetMathJaxContent(elements.answerContent));
505
- elements.thoughtsBox.classList.add('open');
506
- elements.savedModal.classList.remove('active');
507
- resetSolutionTimer(); // Pas de timer pour les solutions sauvegardées
 
508
  }
509
- } else if (deleteName) {
510
- Swal.fire({
511
- title: `Supprimer "${deleteName}" ?`,
512
- text: "Cette action est irréversible.",
513
- icon: 'warning',
514
- showCancelButton: true,
515
- confirmButtonColor: '#d33',
516
- cancelButtonColor: '#3085d6',
517
- confirmButtonText: 'Oui, supprimer !',
518
- cancelButtonText: 'Annuler'
519
- }).then((result) => {
520
- if (result.isConfirmed) {
521
- let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
522
- delete savedExercises[deleteName];
523
- localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
524
- loadSavedList(); // Refresh list
525
- Swal.fire('Supprimé!', `"${deleteName}" a été supprimé.`, 'success');
526
- }
527
- });
 
528
  }
529
  });
530
 
531
- elements.openSaved.addEventListener('click', () => { loadSavedList(); elements.savedModal.classList.add('active'); });
532
- elements.closeSaved.addEventListener('click', () => elements.savedModal.classList.remove('active'));
533
- elements.newExerciseFromModal.addEventListener('click', () => {
534
- resetUIForNewProblem();
535
- elements.savedModal.classList.remove('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  });
537
-
538
- // --- Initialization ---
539
- resetUIForNewProblem(); // Initial UI state including cooldown check
540
- // updateSubmitButtonState(); // Already called by resetUIForNewProblem
541
-
542
- // Fermer le modal si on clique à l'extérieur du contenu (optionnel)
543
- elements.savedModal.addEventListener('click', (e) => {
544
- if (e.target === elements.savedModal) {
545
- elements.savedModal.classList.remove('active');
546
- }
547
- });
548
-
549
  });
550
  </script>
551
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mariam M-1 | Solution Mathématique</title>
7
  <!-- Tailwind CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
9
+
10
  <!-- SweetAlert2 -->
11
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
12
+
13
+ <!-- Highlight.js pour la coloration syntaxique -->
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
16
+
17
+ <!-- Configuration de MathJax -->
18
  <script>
19
  window.MathJax = {
20
  tex: {
 
34
  </script>
35
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
36
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
37
+
38
  <style>
39
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
40
  body { font-family: 'Space Grotesk', sans-serif; }
41
+
42
+ .uploadArea {
43
+ background: #f3f4f6;
44
+ border: 2px dashed #d1d5db;
45
+ transition: border-color 0.2s ease;
46
+ }
47
  .uploadArea:hover { border-color: #3b82f6; }
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;
56
+ height: 48px;
57
+ border: 3px solid #3b82f6;
58
+ border-bottom-color: transparent;
59
+ border-radius: 50%;
60
+ display: inline-block;
61
+ animation: rotation 1s linear infinite;
62
  }
63
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
64
+
65
+ .thought-box {
66
+ transition: max-height 0.3s ease-out;
67
+ max-height: 0;
68
+ overflow: hidden;
69
+ }
70
+ .thought-box.open { max-height: 500px; }
71
+
72
  #thoughtsContent, #answerContent {
73
+ max-height: 500px;
74
+ overflow-y: auto;
75
+ scroll-behavior: smooth;
76
+ white-space: pre-wrap;
77
  }
78
+
79
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
80
+
81
  .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
82
+
83
+ table {
84
+ border-collapse: collapse;
85
+ width: 100%;
86
+ margin-bottom: 1rem;
87
+ }
88
+ th, td {
89
+ border: 1px solid #d1d5db;
90
+ padding: 0.5rem;
91
+ text-align: left;
92
+ }
93
  th { background-color: #f3f4f6; font-weight: 600; }
94
  .table-responsive { overflow-x: auto; }
95
+
96
  #saveButton {
97
+ background: #3b82f6;
98
+ color: white;
99
+ padding: 0.5rem 1rem;
100
+ border-radius: 0.375rem;
101
+ transition: background-color 0.2s ease;
102
  }
103
  #saveButton:hover { background: #2563eb; }
104
+
105
  #savedModal {
106
+ display: none;
107
+ position: fixed;
108
+ inset: 0;
109
+ background: rgba(0,0,0,0.5);
110
+ z-index: 50;
111
  }
112
  #savedModal.active { display: block; }
113
+ #savedModalContent {
114
+ background: #fff;
115
+ width: 100%;
116
+ height: 100%;
117
+ overflow-y: auto;
118
+ }
119
+
120
+ pre {
121
+ background-color: #f8f8f8;
122
+ border: 1px solid #e2e8f0;
123
+ border-radius: 0.375rem;
124
+ padding: 1rem;
125
+ margin: 1rem 0;
126
+ overflow-x: auto;
127
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
128
+ }
129
+
130
+ code {
131
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
132
+ }
133
+
134
+ .code-execution-result {
135
+ background-color: #f0fff4;
136
+ border-left: 4px solid #48bb78;
137
+ padding: 1rem;
138
+ margin: 1rem 0;
139
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
140
+ white-space: pre-wrap;
141
+ }
142
+
143
+ .content-text {}
144
+ .content-code { padding: 0; }
145
+ .content-result {
146
+ background-color: #f0fff4;
147
+ border-left: 4px solid #48bb78;
148
+ padding: 1rem;
149
+ margin: 0.5rem 0;
150
+ }
151
+ .content-image { margin: 1rem 0; text-align: center; }
152
+ .content-image img { max-width: 100%; }
153
+
154
  </style>
155
  </head>
156
+ <body class="p-4">
157
+ <div class="max-w-4xl mx-auto">
158
+ <header class="p-6 text-center mb-8">
159
+ <h1 class="text-4xl font-bold text-blue-600">Mariam M-1(avec calculatrice)</h1>
160
+ <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente, avec intégration d'une calculatrice.</p>
 
 
 
161
  <div class="mt-4 flex justify-end">
162
  <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
163
  </div>
 
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">
171
  <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
172
  <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
 
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>
 
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">
197
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
198
  <span id="timestamp" class="timestamp"></span>
199
  </button>
200
+ <div id="thoughtsBox" class="thought-box">
201
  <div id="thoughtsContent" class="p-4 text-gray-600"></div>
202
  </div>
203
  </div>
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>
210
  </div>
211
  </section>
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">
218
+ <h2 class="text-2xl font-bold">Sauvegardes</h2>
219
+ <button id="closeSaved" class="text-3xl text-gray-600">×</button>
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">
226
+ <button id="newExercise" class="blue-button w-full py-3 text-white font-medium rounded-lg">
227
+ Résoudre un nouvel exercice
228
  </button>
229
  </div>
230
  </div>
 
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');
238
+ const solutionSection = document.getElementById('solution');
239
+ const thoughtsContent = document.getElementById('thoughtsContent');
240
+ const answerContent = document.getElementById('answerContent');
241
+ const thoughtsToggle = document.getElementById('thoughtsToggle');
242
+ const thoughtsBox = document.getElementById('thoughtsBox');
243
+ const imagePreview = document.getElementById('imagePreview');
244
+ const previewImage = document.getElementById('previewImage');
245
+ const timestamp = document.getElementById('timestamp');
246
+ const saveButton = document.getElementById('saveButton');
247
+ const openSaved = document.getElementById('openSaved');
248
+ const closeSaved = document.getElementById('closeSaved');
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;
257
+ let thoughtsBuffer = '';
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);
328
+ timestamp.textContent = `${seconds}s`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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();
338
  reader.onload = e => {
339
+ previewImage.src = e.target.result;
340
+ imagePreview.classList.remove('hidden');
341
  };
342
  reader.readAsDataURL(file);
343
  };
344
 
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
 
369
+ const updateDisplay = async () => {
370
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
371
+ answerContent.innerHTML = marked.parse(answerBuffer);
372
+ await typesetAnswerIfReady();
373
+ updateTimeout = null;
 
 
 
 
 
 
 
374
  };
375
 
376
+ const scheduleUpdate = () => { if (!updateTimeout) updateTimeout = setTimeout(updateDisplay, 200); };
 
 
377
 
378
+ marked.setOptions({
379
+ gfm: true,
380
+ breaks: true,
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({
414
+ icon: 'error',
415
+ title: 'Image manquante',
416
+ text: 'Veuillez sélectionner une image.'
417
+ });
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');
428
+ thoughtsContent.innerHTML = '';
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) {
442
+ throw new Error('Response body is not available (e.g., not a streaming response)');
443
  }
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 {
460
+ const data = JSON.parse(line.slice(5));
 
461
 
462
+ if (data.mode) {
463
+ currentMode = data.mode;
464
+ loader.classList.add('hidden');
465
+ solutionSection.classList.remove('hidden');
466
+ }
467
 
468
+ if (data.content !== undefined) {
469
+ if (currentMode === 'thinking') {
470
+ thoughtsBuffer += data.content;
471
+ } else if (currentMode === 'answering') {
472
+ switch(data.type) {
473
+ case 'code':
474
+ answerBuffer += "\n```" + (data.language || '') + "\n" + data.content + "\n```\n";
475
+ break;
476
+ case 'result':
477
+ const formattedResult = data.content.split('\n').map(line => `> ${line}`).join('\n');
478
+ answerBuffer += "\n" + formattedResult + "\n";
479
+ break;
480
+ case 'image':
481
+ answerBuffer += `\n![Image générée](data:image/png;base64,${data.content})\n`;
482
+ break;
483
+ case 'text':
484
+ default:
485
+ answerBuffer += data.content;
486
+ break;
487
+ }
488
+ }
489
  }
490
+ if (data.error) { answerBuffer += `\n**Erreur:** ${data.error}\n`; }
491
+ } catch (e) {
492
+ console.error('Error parsing JSON data:', line.slice(5), e);
493
+ if (currentMode === 'thinking') { thoughtsBuffer += `\n[Erreur de Parsing des Données]`; }
494
+ else { answerBuffer += `\n[Erreur de Parsing des Données]`; }
495
  }
496
+ }
497
+ scheduleUpdate();
498
+ };
499
+
500
+ while (true) {
501
+ const { done, value } = await reader.read();
502
+ if (done) {
503
+ if (streamBuffer.startsWith('data:')) { // Traiter le dernier morceau s'il existe
504
+ try {
505
+ const data = JSON.parse(streamBuffer.slice(5));
506
+ if (data.content !== undefined) {
507
+ if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
508
+ else if (currentMode === 'answering') { answerBuffer += data.content; } // Simplifié, ajuster si types nécessaires ici
509
+ }
510
+ } catch(e) { console.error("Error parsing final buffer chunk:", e); }
511
+ } else if (streamBuffer.trim() !== '') {
512
+ console.warn("Final stream buffer part was not a data line:", streamBuffer);
513
  }
514
+ scheduleUpdate();
515
+ break;
516
  }
517
+ await processChunk(value);
518
  }
519
+ stopTimer();
520
  } catch (error) {
521
+ console.error('Erreur de Fetch ou du Stream:', error);
522
+ Swal.fire({
523
+ icon: 'error',
524
+ title: 'Erreur de connexion ou de traitement',
525
+ text: `Une erreur est survenue: ${error.message}`
526
+ });
527
+ loader.classList.add('hidden');
528
+ stopTimer();
529
+ solutionSection.classList.remove('hidden');
530
+ if(answerBuffer === '') {
531
+ answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
532
+ } else {
533
+ answerBuffer += `\n\n<div class="text-red-500 p-2 my-2 border border-red-500 rounded">Une erreur est survenue pendant le traitement: ${error.message}</div>`;
534
+ scheduleUpdate();
535
+ }
536
+ // Le cooldown continue indépendamment de l'erreur de fetch.
537
  }
538
  });
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
+ saveButton.addEventListener('click', () => {
542
+ Swal.fire({
543
+ title: 'Sauvegarder la solution',
544
+ input: 'text',
545
+ inputLabel: 'Nom de la sauvegarde',
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>
591
  `;
592
+ savedList.appendChild(li);
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>
665
  </body>