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

Update templates/index.html

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