Docfile commited on
Commit
ec9a9bd
·
verified ·
1 Parent(s): e43a3ec

Update templates/maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +220 -251
templates/maj.html CHANGED
@@ -1,65 +1,52 @@
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 | 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
- <!-- Configuration MathJax -->
 
 
 
13
  <script>
14
  window.MathJax = {
15
  tex: {
16
  inlineMath: [['$', '$']],
17
  displayMath: [['$$', '$$']],
18
  processEscapes: true,
19
- packages: {'[+]': ['autoload','ams']}
20
  },
21
  options: {
22
  enableMenu: false,
23
  messageStyle: 'none'
24
  },
25
  startup: {
26
- pageReady: () => {
27
- console.log('MathJax est complètement chargé.');
28
- window.mathJaxReady = true;
29
- }
30
  }
31
  };
32
  </script>
33
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
34
- <!-- Marked.js - Toujours inclus au cas où il serait utilisé pour thoughtsContent -->
35
- <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
36
 
37
  <style>
38
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
39
-
40
- body {
41
- font-family: 'Space Grotesk', sans-serif;
42
- }
43
-
44
  .uploadArea {
45
  background: #f3f4f6;
46
  border: 2px dashed #d1d5db;
47
  transition: border-color 0.2s ease;
48
  }
49
- .uploadArea:hover {
50
- border-color: #3b82f6;
51
- }
52
-
53
- .blue-button {
54
- background: #3b82f6;
55
- transition: background-color 0.2s ease;
56
- }
57
- .blue-button:hover {
58
- background: #2563eb;
59
- }
60
-
61
- /* Suppression du style du bouton de téléchargement */
62
-
63
  .loader {
64
  width: 48px;
65
  height: 48px;
@@ -69,111 +56,113 @@
69
  display: inline-block;
70
  animation: rotation 1s linear infinite;
71
  }
72
- @keyframes rotation {
73
- 0% { transform: rotate(0deg); }
74
- 100% { transform: rotate(360deg); }
75
- }
76
-
77
  .thought-box {
78
  transition: max-height 0.3s ease-out;
79
  max-height: 0;
80
  overflow: hidden;
81
  }
82
- .thought-box.open {
83
- max-height: 500px; /* Ajusté si nécessaire */
84
- }
85
-
86
  #thoughtsContent, #answerContent {
87
  max-height: 500px;
88
  overflow-y: auto;
89
  scroll-behavior: smooth;
90
- white-space: pre-wrap; /* Important pour préserver les espaces/retours ligne du LaTeX brut */
91
- }
92
-
93
- .preview-image {
94
- max-width: 300px;
95
- max-height: 300px;
96
- object-fit: contain;
97
  }
98
-
99
- .timestamp {
100
- color: #3b82f6;
101
- font-size: 0.9em;
102
- margin-left: 8px;
103
- }
104
-
105
- /* Styles pour les tables générées par MathJax/Markdown si nécessaire */
106
  table {
107
  border-collapse: collapse;
108
  width: 100%;
109
  margin-bottom: 1rem;
110
- border: 1px solid #d1d5db; /* Ajout bordure table */
111
  }
112
  th, td {
113
  border: 1px solid #d1d5db;
114
  padding: 0.5rem;
115
  text-align: left;
116
  }
117
- th {
118
- background-color: #f3f4f6;
119
- font-weight: 600;
 
 
 
 
 
 
 
120
  }
121
- .table-responsive {
122
- overflow-x: auto;
 
 
 
 
 
 
 
123
  }
124
-
125
- /* Suppression du style pour l'impression */
126
-
127
- .performance-warning {
128
- color: red;
129
- font-weight: bold;
130
- font-size: 1.2em;
131
- margin-top: 10px;
132
- margin-bottom: 25px;
133
- text-align: center;
134
  }
135
  </style>
136
  </head>
137
  <body class="p-4">
138
  <div class="max-w-4xl mx-auto">
139
  <header class="p-6 text-center mb-8">
140
- <h1 class="text-4xl font-bold text-blue-600">Mariam - M-0</h1>
141
  <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
142
  <p class="performance-warning">
143
  Vous utilisez actuellement les modèles/performances moyens. Accédez à des performances supérieures avec un abonnement premium !
144
- </p>
 
 
 
 
145
  </header>
146
 
147
- <main>
148
  <form id="problemForm" class="space-y-6" novalidate>
 
149
  <div class="uploadArea p-8 text-center relative" aria-label="Zone de dépôt d'image">
150
  <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
151
  <div class="space-y-3">
152
  <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
153
  <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
154
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
 
155
  </svg>
156
  </div>
157
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
158
  <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
159
  </div>
160
  </div>
161
-
162
  <div id="imagePreview" class="hidden text-center">
163
- <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image sélectionnée">
164
  </div>
165
-
166
  <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
167
  Résoudre le problème
168
  </button>
169
  </form>
170
 
 
171
  <div id="loader" class="hidden mt-8 text-center">
172
  <span class="loader"></span>
173
  <p class="mt-4 text-gray-600">Analyse en cours...</p>
174
  </div>
175
 
176
- <section id="solution" class="hidden mt-8 space-y-6">
 
177
  <div class="border-t pt-4">
178
  <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
179
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
@@ -184,9 +173,10 @@
184
  </div>
185
  </div>
186
  <div class="border-t pt-6">
187
- <div class="flex justify-between items-center mb-4">
188
- <h3 class="text-xl font-bold text-gray-800">Solution</h3>
189
- <!-- Suppression du bouton de téléchargement -->
 
190
  </div>
191
  <div id="answerContent" class="text-gray-700 table-responsive"></div>
192
  </div>
@@ -194,8 +184,29 @@
194
  </main>
195
  </div>
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  <script>
198
  document.addEventListener('DOMContentLoaded', () => {
 
199
  const form = document.getElementById('problemForm');
200
  const imageInput = document.getElementById('imageInput');
201
  const loader = document.getElementById('loader');
@@ -207,6 +218,13 @@
207
  const imagePreview = document.getElementById('imagePreview');
208
  const previewImage = document.getElementById('previewImage');
209
  const timestamp = document.getElementById('timestamp');
 
 
 
 
 
 
 
210
 
211
  let startTime = null;
212
  let timerInterval = null;
@@ -215,28 +233,17 @@
215
  let currentMode = null;
216
  let updateTimeout = null;
217
 
218
- // Suppression de la fonction generatePDF et de son appel
219
-
220
  const updateTimestamp = () => {
221
  if (startTime) {
222
  const seconds = Math.floor((Date.now() - startTime) / 1000);
223
  timestamp.textContent = `${seconds}s`;
224
  }
225
  };
 
 
226
 
227
- const startTimer = () => {
228
- startTime = Date.now();
229
- timerInterval = setInterval(updateTimestamp, 1000);
230
- updateTimestamp();
231
- };
232
-
233
- const stopTimer = () => {
234
- clearInterval(timerInterval);
235
- startTime = null;
236
- // Ne pas effacer le timestamp final
237
- // timestamp.textContent = '';
238
- };
239
-
240
  const handleFileSelect = file => {
241
  if (!file) return;
242
  const reader = new FileReader();
@@ -247,95 +254,38 @@
247
  reader.readAsDataURL(file);
248
  };
249
 
250
- thoughtsToggle.addEventListener('click', () => {
251
- thoughtsBox.classList.toggle('open');
252
- });
253
-
254
  imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
255
 
 
256
  const dropZone = document.querySelector('.uploadArea');
257
- dropZone.addEventListener('dragover', e => {
258
- e.preventDefault();
259
- dropZone.classList.add('border-blue-400');
260
- });
261
- dropZone.addEventListener('dragleave', e => {
262
- e.preventDefault();
263
- dropZone.classList.remove('border-blue-400');
264
- });
265
- dropZone.addEventListener('drop', e => {
266
- e.preventDefault();
267
- dropZone.classList.remove('border-blue-400');
268
- handleFileSelect(e.dataTransfer.files[0]);
269
- });
270
-
271
- // --- Déclaration de typesetContentIfReady ---
272
- const typesetContentIfReady = async () => {
273
- if (window.mathJaxReady && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
274
- // Cible les deux conteneurs si nécessaire, ou juste answerContent si seul lui contient du LaTeX
275
- // Pour être sûr, on peut cibler la section solution entière ou des éléments spécifiques.
276
- // Ici, on cible explicitement answerContent comme dans le code original.
277
- // Si thoughtsContent peut aussi avoir du MathJax, ajoutez-le au tableau.
278
- // MathJax.startup.document.elements = [answerContent, thoughtsContent];
279
- MathJax.startup.document.elements = [answerContent]; // Cible uniquement answerContent
280
- try {
281
- await MathJax.typesetPromise();
282
- // Fait défiler vers le bas après le rendu MathJax
283
- answerContent.scrollTop = answerContent.scrollHeight;
284
- // Si thoughtsContent est aussi traité:
285
- // thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
286
- } catch (error) {
287
- console.error("Erreur pendant MathJax typesetPromise:", error);
288
- }
289
- } else {
290
- console.log('MathJax pas prêt, report du rendu...');
291
- setTimeout(typesetContentIfReady, 200); // Réessayer
292
- }
293
  };
294
-
295
- // --- Déclaration de updateDisplay ---
296
  const updateDisplay = async () => {
297
- // Utilise Marked pour thoughtsContent (supposant qu'il contient du Markdown)
298
- if (typeof marked !== 'undefined') {
299
- thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
300
- } else {
301
- thoughtsContent.textContent = thoughtsBuffer; // Fallback si Marked n'est pas chargé
302
- }
303
-
304
- // **CORRECTION:** Insère le contenu brut dans answerContent, sans passer par Marked
305
- answerContent.innerHTML = answerBuffer;
306
-
307
- // Déclenche le rendu MathJax sur le contenu mis à jour
308
- await typesetContentIfReady();
309
-
310
- // Scrolling (peut être redondant si fait dans typesetContentIfReady)
311
- thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
312
- // answerContent.scrollTop = answerContent.scrollHeight; // Déplacé dans typesetContentIfReady
313
-
314
  updateTimeout = null;
315
  };
 
316
 
317
- const scheduleUpdate = () => {
318
- if (updateTimeout) return;
319
- // Délai légèrement augmenté pour laisser le temps au DOM de se mettre à jour avant MathJax
320
- updateTimeout = setTimeout(updateDisplay, 250);
321
- };
322
-
323
- // Configure Marked (si utilisé pour thoughtsContent)
324
- if (typeof marked !== 'undefined') {
325
- marked.setOptions({
326
- gfm: true,
327
- breaks: true
328
- });
329
- }
330
 
 
331
  form.addEventListener('submit', async e => {
332
  e.preventDefault();
333
  const file = imageInput.files[0];
334
- if (!file) {
335
- alert('Veuillez sélectionner une image.');
336
- return;
337
- }
338
-
339
  startTimer();
340
  loader.classList.remove('hidden');
341
  solutionSection.classList.add('hidden');
@@ -344,104 +294,123 @@
344
  thoughtsBuffer = '';
345
  answerBuffer = '';
346
  currentMode = null;
347
- thoughtsBox.classList.add('open'); // Ouvre par défaut
348
 
349
  const formData = new FormData();
350
  formData.append('image', file);
351
-
352
  try {
353
- const response = await fetch('/solved', {
354
- method: 'POST',
355
- body: formData
356
- });
357
-
358
- if (!response.ok) {
359
- throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
360
- }
361
- if (!response.body) {
362
- throw new Error("La réponse ne contient pas de corps.");
363
- }
364
-
365
  const reader = response.body.getReader();
366
  const decoder = new TextDecoder();
367
  let buffer = '';
368
- let firstChunkReceived = false; // Pour afficher la section dès réception
369
-
370
- const processChunk = async ({ done, value }) => {
371
- if (done) {
372
- // Traiter le reste du buffer s'il y en a
373
- if (buffer && buffer.startsWith('data:')) {
374
- try {
375
- const data = JSON.parse(buffer.slice(5));
376
- if (data.content) {
377
- if (currentMode === 'thinking') {
378
- thoughtsBuffer += data.content;
379
- } else if (currentMode === 'answering') {
380
- answerBuffer += data.content;
381
- }
382
- }
383
- } catch(jsonError){
384
- console.error("Erreur JSON dans le buffer final:", jsonError, "Buffer:", buffer);
385
- }
386
- }
387
- // Assurer une dernière mise à jour après la fin du stream
388
- await updateDisplay();
389
- stopTimer(); // Arrêter le timer à la fin
390
- console.log("Stream terminé.");
391
- return true; // Indique que le stream est terminé
392
- }
393
-
394
- buffer += decoder.decode(value, { stream: true });
395
  const lines = buffer.split('\n\n');
396
- buffer = lines.pop() || ''; // Garde la partie incomplète pour le prochain chunk
397
-
398
  for (const line of lines) {
399
  if (!line.startsWith('data:')) continue;
400
- try {
401
- const data = JSON.parse(line.slice(5));
402
-
403
- if (data.mode) {
404
- currentMode = data.mode;
405
- // Afficher la section dès la première donnée reçue après le début du mode
406
- if (!firstChunkReceived) {
407
- loader.classList.add('hidden');
408
- solutionSection.classList.remove('hidden');
409
- firstChunkReceived = true;
410
- }
411
- }
412
- if (data.content) {
413
- if (currentMode === 'thinking') {
414
- thoughtsBuffer += data.content;
415
- } else if (currentMode === 'answering') {
416
- answerBuffer += data.content;
417
- }
418
- // Mise à jour programmée pour éviter trop d'appels DOM/MathJax
419
- scheduleUpdate();
420
- }
421
- } catch(jsonError) {
422
- console.error("Erreur JSON dans le stream:", jsonError, "Ligne:", line);
423
- // Continuer avec les lignes suivantes si possible
424
  }
425
  }
426
- return false; // Indique que le stream n'est pas terminé
427
  };
428
-
429
- // Boucle de lecture du stream
430
- let streamFinished = false;
431
- while (!streamFinished) {
432
- const { done, value } = await reader.read();
433
- streamFinished = await processChunk({ done, value });
 
 
 
 
 
 
 
 
434
  }
435
-
436
  } catch (error) {
437
- console.error('Erreur lors de la récupération ou du traitement:', error);
438
- alert(`Une erreur est survenue: ${error.message}`);
439
  loader.classList.add('hidden');
440
- solutionSection.classList.add('hidden'); // Cacher si erreur
441
  stopTimer();
442
  }
443
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  });
445
  </script>
446
  </body>
447
- </html>
 
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: {
19
  inlineMath: [['$', '$']],
20
  displayMath: [['$$', '$$']],
21
  processEscapes: true,
22
+ packages: {'[+]': ['autoload', 'ams']}
23
  },
24
  options: {
25
  enableMenu: false,
26
  messageStyle: 'none'
27
  },
28
  startup: {
29
+ pageReady: () => { window.mathJaxReady = true; }
 
 
 
30
  }
31
  };
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;
 
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;
106
+ inset: 0;
107
+ background: rgba(0,0,0,0.5);
108
+ z-index: 50;
109
  }
110
+ #savedModal.active { display: block; }
111
+ #savedModalContent {
112
+ background: #fff;
113
+ width: 100%;
114
+ height: 100%;
115
+ overflow-y: auto;
 
 
 
 
116
  }
117
  </style>
118
  </head>
119
  <body class="p-4">
120
  <div class="max-w-4xl mx-auto">
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>
131
  </header>
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">
139
  <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
140
  <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
141
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
142
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
143
  </svg>
144
  </div>
145
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
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>
 
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
  </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>
204
+ </div>
205
+ </div>
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');
 
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;
 
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();
 
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');
 
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>