Docfile commited on
Commit
f1d21c2
·
verified ·
1 Parent(s): f2c8b1b

Update templates/maj.html

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