Docfile commited on
Commit
0c39a6f
·
verified ·
1 Parent(s): fb4ab9a

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +624 -410
templates/index.html CHANGED
@@ -1,437 +1,651 @@
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>Résolution d'exercices avec Gemini</title>
7
-
8
- <!-- Inclusion de Tailwind CSS via CDN -->
9
- <script src="https://cdn.tailwindcss.com"></script>
10
-
11
- <!-- *** NOUVEAU: Inclusion de marked.js *** -->
12
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
13
-
14
- <!-- Inclusion de MathJax pour le rendu LaTeX -->
15
- <script>
16
- MathJax = {
17
- tex: {
18
- inlineMath: [['$', '$'], ['\\(', '\\)']],
19
- displayMath: [['$$', '$$'], ['\\[', '\\]']],
20
- processEscapes: true
21
- },
22
- svg: {
23
- fontCache: 'global'
24
- },
25
- // *** IMPORTANT: Désactiver le rendu initial automatique ***
26
- // Nous le déclencherons manuellement après avoir ajouté du contenu.
27
- startup: {
28
- ready: () => {
29
- console.log('MathJax is ready');
30
- MathJax.startup.defaultReady();
31
- // Ne pas faire de typeset initial ici
32
- // MathJax.startup.promise.then(() => { console.log('Initial typeset done'); });
33
- }
34
- }
35
- };
36
- </script>
37
- <script type="text/javascript" id="MathJax-script" async
38
- src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
39
- </script>
40
-
41
- <!-- Inclusion de highlight.js pour la coloration syntaxique -->
42
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
43
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
44
-
45
- <!-- Styles personnalisés -->
46
- <style>
47
- mjx-container {
48
- line-height: normal !important;
49
- display: inline-block !important; /* Important pour l'alignement */
50
- margin: 0 0.15em; /* Ajouter un petit espace autour */
51
- vertical-align: middle; /* Essayer d'aligner verticalement */
52
- }
53
- /* S'assurer que le code dans prose est stylisé par highlight.js */
54
- .prose pre code.hljs {
55
- border-radius: 0.375rem; /* rounded-md */
56
- padding: 1rem; /* p-4 */
57
- /* La couleur de fond est définie par le thème hljs */
58
- }
59
- /* Empêcher prose de trop styliser les blocs de code */
60
- .prose pre {
61
- background-color: transparent !important;
62
- padding: 0 !important;
63
- margin-top: 1em;
64
- margin-bottom: 1em;
65
- }
66
- /* Amélioration pour les listes dans le markdown rendu */
67
- .prose ul, .prose ol {
68
- margin-left: 1.5rem;
69
- }
70
- /* Styles pour le résultat de code */
71
- .code-result {
72
- margin-top: 0.5rem; /* Espacement avant */
73
- margin-bottom: 1rem; /* Espacement après */
74
- padding: 0.75rem 1rem; /* p-3 p-4 */
75
- background-color: #f0fff4; /* bg-green-50 */
76
- border-left: 4px solid #38a169; /* border-l-4 border-green-500 */
77
- font-size: 0.875rem; /* text-sm */
78
- color: #2f855a; /* text-green-800 */
79
- border-radius: 0 0.375rem 0.375rem 0; /* rounded-r-lg */
80
- white-space: pre-wrap; /* Conserver les sauts de ligne */
81
- word-wrap: break-word; /* Retour à la ligne si nécessaire */
82
- }
83
- /* Styles pour les indicateurs */
84
- .indicator {
85
- margin: 0.75rem 0; /* my-3 */
86
- text-align: center;
87
- font-style: italic;
88
- color: #6b7280; /* text-gray-500 */
89
- }
90
- /* Styles pour les messages d'erreur */
91
- .error-message {
92
- margin: 1rem 0; /* my-4 */
93
- padding: 1rem; /* p-4 */
94
- background-color: #fee2e2; /* bg-red-100 */
95
- border: 1px solid #fca5a5; /* border-red-300 */
96
- color: #b91c1c; /* text-red-700 */
97
- border-radius: 0.5rem; /* rounded-lg */
98
- }
99
- /* Styles pour les images générées */
100
- .generated-image {
101
- max-width: 100%;
102
- height: auto;
103
- margin: 1rem auto; /* my-4 mx-auto */
104
- border: 1px solid #d1d5db; /* border-gray-300 */
105
- border-radius: 0.25rem; /* rounded */
106
- padding: 0.25rem; /* p-1 */
107
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow-sm */
108
- display: block; /* block */
109
- }
110
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  </head>
112
- <body class="bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-100 text-gray-800 font-sans min-h-screen flex flex-col items-center py-12 px-4">
113
-
114
- <div class="w-full max-w-3xl">
115
- <h1 class="text-4xl font-bold text-center mb-10 text-indigo-700">
116
- Résolution d'exercices avec Gemini
117
- </h1>
118
-
119
- <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
120
- <!-- Section Mode Normal -->
121
- <div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
122
- <h2 class="text-2xl font-semibold mb-5 text-indigo-600">Mode Normal (Gemini Pro)</h2>
123
- <form id="solve-form" enctype="multipart/form-data">
124
- <div class="mb-4">
125
- <label for="image" class="block text-sm font-medium text-gray-700 mb-2">
126
- Téléchargez une image de votre exercice :
127
- </label>
128
- <input type="file" id="image" name="image" accept="image/*" required
129
- class="block w-full text-sm text-gray-500 border border-gray-300 rounded-lg cursor-pointer
130
- file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0
131
- file:text-sm file:font-semibold file:bg-indigo-100 file:text-indigo-700
132
- hover:file:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
133
- </div>
134
- <button type="submit"
135
- class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg
136
- focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-50
137
- transition duration-150 ease-in-out">
138
- Résoudre
139
- </button>
140
- </form>
141
- </div>
142
 
143
- <!-- Section Mode Rapide -->
144
- <div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
145
- <h2 class="text-2xl font-semibold mb-5 text-purple-600">Mode Rapide (Gemini Flash)</h2>
146
- <form id="solved-form" enctype="multipart/form-data">
147
- <div class="mb-4">
148
- <label for="image2" class="block text-sm font-medium text-gray-700 mb-2">
149
- Téléchargez une image de votre exercice :
150
- </label>
151
- <input type="file" id="image2" name="image" accept="image/*" required
152
- class="block w-full text-sm text-gray-500 border border-gray-300 rounded-lg cursor-pointer
153
- file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0
154
- file:text-sm file:font-semibold file:bg-purple-100 file:text-purple-700
155
- hover:file:bg-purple-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
156
- </div>
157
- <button type="submit"
158
- class="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded-lg
159
- focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50
160
- transition duration-150 ease-in-out">
161
- Résoudre rapidement
162
- </button>
163
- </form>
164
  </div>
 
 
 
165
  </div>
166
-
167
- <!-- Conteneur de Résultats -->
168
- <!-- Ajout de classes 'prose' pour le style Markdown/texte par défaut -->
169
- <div id="result-container" class="bg-white p-6 rounded-xl shadow-lg border border-gray-200 w-full min-h-[150px] prose max-w-none prose-indigo">
170
- <p class="text-gray-500 italic text-center">Les résultats apparaîtront ici...</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- <!-- Inclusion du script JS personnalisé (mis à jour) -->
175
- <script>
176
- document.addEventListener('DOMContentLoaded', function() {
177
- const resultContainer = document.getElementById('result-container');
178
-
179
- // Configure marked.js (optionnel, mais utile)
180
- marked.setOptions({
181
- highlight: function(code, lang) {
182
- const language = hljs.getLanguage(lang) ? lang : 'plaintext';
183
- return hljs.highlight(code, { language }).value;
184
- },
185
- langPrefix: 'hljs language-', // pour la compatibilité avec hljs CSS
186
- gfm: true, // Activer GitHub Flavored Markdown
187
- breaks: true, // Convertir les sauts de ligne simples en <br>
188
- });
189
-
190
- // Fonction pour créer un élément de code formaté et le colorer
191
- function createCodeElement(code, language = null) {
192
- const pre = document.createElement('pre');
193
- // Note: La classe 'not-prose' empêche prose d'interférer avec le style hljs
194
- pre.className = 'not-prose'; // Empêche le style prose d'affecter ce bloc
195
-
196
- const codeEl = document.createElement('code');
197
- // Ajouter la classe de langage si spécifié pour highlight.js
198
- if (language) {
199
- codeEl.className = `language-${language}`;
200
- } else {
201
- codeEl.className = 'language-plaintext'; // Défaut si non spécifié
202
- }
203
- codeEl.textContent = code; // Utiliser textContent pour éviter l'injection HTML
204
-
205
- pre.appendChild(codeEl);
206
- // Appliquer highlight.js à cet élément spécifique
207
- // hljs.highlightElement(codeEl); // marqué.js le fait maintenant via l'option highlight
208
- hljs.highlightBlock(codeEl); // Alternative si on n'utilise pas l'option highlight de marked
209
- return pre;
210
- }
211
-
212
- // *** MODIFIÉ: Fonction pour créer un élément de texte/Markdown ***
213
- function createMarkdownElement(text) {
214
- const div = document.createElement('div');
215
- div.className = 'markdown-content'; // La classe 'prose' sur le parent gère le style
216
- // Utiliser marked.js pour convertir le Markdown en HTML
217
- div.innerHTML = marked.parse(text);
218
- return div;
219
- }
220
-
221
- // Fonction pour créer un élément spécialisé pour le résultat de code
222
- function createResultElement(text) {
223
- const div = document.createElement('div');
224
- // Utilise la classe CSS définie dans <style>
225
- div.className = 'code-result';
226
- // Utiliser textContent pour la sécurité et préserver les espaces/sauts de ligne
227
- div.textContent = text;
228
- return div;
229
- }
230
-
231
-
232
- // Fonction pour créer un élément image
233
- function createImageElement(base64Data, format = 'png') {
234
- const img = document.createElement('img');
235
- img.src = `data:image/${format};base64,${base64Data}`;
236
- // Utilise la classe CSS définie dans <style>
237
- img.className = 'generated-image';
238
- return img;
239
- }
240
 
241
- // Fonction pour créer un indicateur (chargement, réflexion)
242
- function createIndicator(text, type = 'loading') {
243
- const div = document.createElement('div');
244
- div.dataset.indicatorType = type;
245
- // Utilise la classe CSS définie dans <style>
246
- div.className = 'indicator';
247
- div.textContent = text;
248
- return div;
249
- }
250
 
251
- // Fonction pour créer un message d'erreur
252
- function createErrorElement(text) {
253
- const div = document.createElement('div');
254
- // Utilise la classe CSS définie dans <style>
255
- div.className = 'error-message';
256
- div.textContent = 'Erreur: ' + text;
257
- return div;
258
- }
259
 
260
- // Fonction pour traiter les données SSE et mettre à jour l'UI
261
- function processSseData(jsonData) {
262
- // Gestion des modes 'thinking'/'answering'
263
- if (jsonData.mode === 'thinking') {
264
- const loadingIndicator = resultContainer.querySelector('[data-indicator-type="loading"]');
265
- if (loadingIndicator) loadingIndicator.remove();
266
- if (!resultContainer.querySelector('[data-indicator-type="thinking"]')) {
267
- resultContainer.appendChild(createIndicator('Gemini réfléchit...', 'thinking'));
268
- }
269
- } else if (jsonData.mode === 'answering') {
270
- const thinkingIndicator = resultContainer.querySelector('[data-indicator-type="thinking"]');
271
- if (thinkingIndicator) thinkingIndicator.remove();
272
- }
273
-
274
- // Traitement du contenu
275
- if (jsonData.content) {
276
- let element;
277
- let requiresMathJax = false;
278
 
279
- switch(jsonData.type) {
280
- case 'text':
281
- // Utilise la fonction mise à jour avec marked.js
282
- element = createMarkdownElement(jsonData.content);
283
- // Le texte Markdown peut contenir du LaTeX
284
- requiresMathJax = true;
285
- break;
286
- case 'code':
287
- // 'python' pourrait être passé du backend si connu, sinon null
288
- element = createCodeElement(jsonData.content, 'python');
289
- break;
290
- case 'result':
291
- element = createResultElement(jsonData.content);
292
- break;
293
- case 'image':
294
- element = createImageElement(jsonData.content);
295
- break;
296
- default: // Traiter comme du texte par défaut
297
- console.warn("Type de contenu inconnu reçu:", jsonData.type);
298
- element = createMarkdownElement(jsonData.content); // fallback
299
- requiresMathJax = true;
300
- }
301
 
302
- if (element) {
303
- resultContainer.appendChild(element);
 
 
 
 
304
 
305
- // *** IMPORTANT: Déclencher MathJax et Highlight.js ***
306
- // Déclencher MathJax UNIQUEMENT si le contenu ajouté peut contenir du LaTeX
307
- if (requiresMathJax && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
308
- // Typeset le conteneur entier ou juste le nouvel élément.
309
- // Typesetter le conteneur entier peut être plus simple si les éléments arrivent rapidement
310
- MathJax.typesetPromise([resultContainer]).catch((err) => console.error('MathJax processing error:', err));
311
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
- // S'assurer que les blocs de code ajoutés (qui ne sont pas dans le markdown) sont colorisés
314
- // Note: marked.js s'occupe de ceux dans le markdown via l'option `highlight`
315
- if (jsonData.type === 'code') {
316
- // hljs.highlightElement(element.querySelector('code')); // Si createCodeElement n'appelle pas highlightBlock
317
- // Déjà fait dans createCodeElement maintenant.
318
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  }
 
 
 
 
 
320
  }
321
-
322
- // Gestion des erreurs
323
- if (jsonData.error) {
324
- resultContainer.appendChild(createErrorElement(jsonData.error));
325
- resultContainer.querySelectorAll('.indicator').forEach(el => el.remove());
326
- }
327
  }
328
-
329
-
330
- // Fonction pour gérer les événements SSE via Fetch API (inchangée)
331
- async function setupFetchStream(url, formData) {
332
- // ... (le reste de la fonction setupFetchStream reste identique à votre version) ...
333
- // Vider le conteneur de résultats et afficher chargement
334
- resultContainer.innerHTML = '';
335
- resultContainer.appendChild(createIndicator('Chargement en cours...', 'loading'));
336
-
337
  try {
338
- const response = await fetch(url, {
339
- method: 'POST',
340
- body: formData
341
- });
342
-
343
- if (!response.ok) {
344
- // Essayer de lire le corps de l'erreur s'il existe
345
- let errorBody = await response.text();
346
- try {
347
- const errorJson = JSON.parse(errorBody);
348
- errorBody = errorJson.error || JSON.stringify(errorJson);
349
- } catch(e) {
350
- // Le corps n'était pas du JSON, utiliser le texte brut
351
- }
352
- throw new Error(`Erreur HTTP ${response.status}: ${errorBody || response.statusText}`);
353
- }
354
-
355
- // Vider à nouveau au cas où la requête prend du temps avant que le stream commence
356
- // resultContainer.innerHTML = ''; // Peut causer un flash, on le laisse commenté pour l'instant
357
-
358
- const reader = response.body.getReader();
359
- const decoder = new TextDecoder();
360
- let buffer = '';
361
-
362
- while (true) {
363
- const { done, value } = await reader.read();
364
- if (done) break;
365
-
366
- buffer += decoder.decode(value, { stream: true });
367
-
368
- let boundary = buffer.indexOf('\n\n');
369
- while (boundary !== -1) {
370
- const message = buffer.substring(0, boundary);
371
- buffer = buffer.substring(boundary + 2);
372
-
373
- if (message.startsWith('data: ')) {
374
- try {
375
- const jsonData = JSON.parse(message.substring(6));
376
- processSseData(jsonData); // Utilise la fonction de traitement mise à jour
377
- } catch (e) {
378
- console.error('Erreur parsing JSON du SSE:', e, 'Data:', message.substring(6));
379
- // Afficher une erreur à l'utilisateur pour le JSON invalide
380
- resultContainer.appendChild(createErrorElement(`Erreur interne lors du traitement de la réponse: ${e.message}`));
381
- }
382
- }
383
- boundary = buffer.indexOf('\n\n');
384
- }
385
- // Faire défiler vers le bas
386
- // window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
387
- resultContainer.scrollTop = resultContainer.scrollHeight; // Mieux pour un div scrollable
388
- }
389
- // Traiter le reste du buffer
390
- if (buffer.startsWith('data: ')) {
391
- try {
392
- const jsonData = JSON.parse(buffer.substring(6));
393
- processSseData(jsonData);
394
- } catch (e) {
395
- console.error('Erreur parsing JSON du dernier chunk SSE:', e, 'Data:', buffer.substring(6));
396
- resultContainer.appendChild(createErrorElement(`Erreur interne lors du traitement final de la réponse: ${e.message}`));
397
- }
398
- }
399
-
400
- } catch (error) {
401
- console.error('Erreur Fetch Stream:', error);
402
- // Ne pas vider si des messages d'erreur précédents ont été affichés
403
- // resultContainer.innerHTML = '';
404
- // Afficher l'erreur attrapée
405
- resultContainer.appendChild(createErrorElement(error.message));
406
-
407
- } finally {
408
- // Supprimer tous les indicateurs restants
409
- resultContainer.querySelectorAll('.indicator').forEach(el => el.remove());
410
- // Faire défiler vers le bas une dernière fois
411
- resultContainer.scrollTop = resultContainer.scrollHeight;
412
  }
 
 
 
413
  }
414
-
415
- // Gestionnaires d'événements pour les formulaires (inchangés)
416
- const solveForm = document.getElementById('solve-form');
417
- if (solveForm) {
418
- solveForm.addEventListener('submit', function(e) {
419
- e.preventDefault();
420
- const formData = new FormData(solveForm);
421
- setupFetchStream('/solve', formData);
422
- });
423
- }
424
-
425
- const solvedForm = document.getElementById('solved-form');
426
- if (solvedForm) {
427
- solvedForm.addEventListener('submit', function(e) {
428
- e.preventDefault();
429
- const formData = new FormData(solvedForm);
430
- setupFetchStream('/solved', formData);
431
- });
432
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  });
434
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  </body>
437
  </html>
 
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
+
10
+ <!-- SweetAlert2 -->
11
+ <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
12
+
13
+ <!-- Highlight.js pour la coloration syntaxique -->
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
16
+
17
+ <!-- Configuration de MathJax -->
18
+ <script>
19
+ window.MathJax = {
20
+ tex: {
21
+ inlineMath: [['$', '$']],
22
+ displayMath: [['$$', '$$']],
23
+ processEscapes: true,
24
+ packages: {'[+]': ['autoload', 'ams']}
25
+ },
26
+ options: {
27
+ enableMenu: false,
28
+ messageStyle: 'none'
29
+ },
30
+ startup: {
31
+ pageReady: () => { window.mathJaxReady = true; }
32
+ }
33
+ };
34
+ </script>
35
+ <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
36
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
37
+
38
+ <style>
39
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
40
+ body { font-family: 'Space Grotesk', sans-serif; }
41
+
42
+ .uploadArea {
43
+ background: #f3f4f6;
44
+ border: 2px dashed #d1d5db;
45
+ transition: border-color 0.2s ease;
46
+ }
47
+ .uploadArea:hover { border-color: #3b82f6; }
48
+
49
+ .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
50
+ .blue-button:hover { background: #2563eb; }
51
+
52
+ .loader {
53
+ width: 48px;
54
+ height: 48px;
55
+ border: 3px solid #3b82f6;
56
+ border-bottom-color: transparent;
57
+ border-radius: 50%;
58
+ display: inline-block;
59
+ animation: rotation 1s linear infinite;
60
+ }
61
+ @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
62
+
63
+ .thought-box {
64
+ transition: max-height 0.3s ease-out;
65
+ max-height: 0;
66
+ overflow: hidden;
67
+ }
68
+ .thought-box.open { max-height: 500px; }
69
+
70
+ #thoughtsContent, #answerContent {
71
+ max-height: 500px;
72
+ overflow-y: auto;
73
+ scroll-behavior: smooth;
74
+ white-space: pre-wrap;
75
+ }
76
+
77
+ .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
78
+
79
+ .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
80
+
81
+ table {
82
+ border-collapse: collapse;
83
+ width: 100%;
84
+ margin-bottom: 1rem;
85
+ }
86
+ th, td {
87
+ border: 1px solid #d1d5db;
88
+ padding: 0.5rem;
89
+ text-align: left;
90
+ }
91
+ th { background-color: #f3f4f6; font-weight: 600; }
92
+ .table-responsive { overflow-x: auto; }
93
+
94
+ /* Style pour le bouton Sauvegarder afin de le mettre en évidence */
95
+ #saveButton {
96
+ background: #3b82f6;
97
+ color: white;
98
+ padding: 0.5rem 1rem;
99
+ border-radius: 0.375rem;
100
+ transition: background-color 0.2s ease;
101
+ }
102
+ #saveButton:hover { background: #2563eb; }
103
+
104
+ /* Modal plein écran pour les sauvegardes */
105
+ #savedModal {
106
+ display: none;
107
+ position: fixed;
108
+ inset: 0;
109
+ background: rgba(0,0,0,0.5);
110
+ z-index: 50;
111
+ }
112
+ #savedModal.active { display: block; }
113
+ #savedModalContent {
114
+ background: #fff;
115
+ width: 100%;
116
+ height: 100%;
117
+ overflow-y: auto;
118
+ }
119
+
120
+ /* Styles spécifiques pour le code et son exécution */
121
+ pre {
122
+ background-color: #f8f8f8;
123
+ border: 1px solid #e2e8f0;
124
+ border-radius: 0.375rem;
125
+ padding: 1rem;
126
+ margin: 1rem 0;
127
+ overflow-x: auto;
128
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
129
+ }
130
+
131
+ code {
132
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
133
+ }
134
+
135
+ .code-execution-result {
136
+ background-color: #f0fff4;
137
+ border-left: 4px solid #48bb78;
138
+ padding: 1rem;
139
+ margin: 1rem 0;
140
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
141
+ white-space: pre-wrap;
142
+ }
143
+
144
+ /* Styles pour les types de contenu spécifiques */
145
+ .content-text {}
146
+ .content-code { padding: 0; }
147
+ .content-result {
148
+ background-color: #f0fff4;
149
+ border-left: 4px solid #48bb78;
150
+ padding: 1rem;
151
+ margin: 0.5rem 0;
152
+ }
153
+ .content-image { margin: 1rem 0; text-align: center; }
154
+ .content-image img { max-width: 100%; }
155
+
156
+ /* Onglets pour choisir le modèle */
157
+ .tabs {
158
+ display: flex;
159
+ border-bottom: 1px solid #e2e8f0;
160
+ margin-bottom: 1rem;
161
+ }
162
+
163
+ .tab {
164
+ padding: 0.5rem 1rem;
165
+ cursor: pointer;
166
+ border-bottom: 2px solid transparent;
167
+ }
168
+
169
+ .tab.active {
170
+ border-bottom: 2px solid #3b82f6;
171
+ color: #3b82f6;
172
+ font-weight: 600;
173
+ }
174
+ </style>
175
  </head>
176
+ <body class="p-4">
177
+ <div class="max-w-4xl mx-auto">
178
+ <header class="p-6 text-center mb-8">
179
+ <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
180
+ <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
181
+ <div class="mt-4 flex justify-end">
182
+ <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
183
+ </div>
184
+ </header>
185
+
186
+ <main id="mainContent">
187
+ <form id="problemForm" class="space-y-6" novalidate>
188
+ <!-- Onglets pour choisir le modèle -->
189
+ <div class="tabs">
190
+ <div class="tab active" data-endpoint="solve">Gemini Pro</div>
191
+ <div class="tab" data-endpoint="solved">Gemini Flash</div>
192
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ <!-- Zone de dépôt / sélection d'image -->
195
+ <div class="uploadArea p-8 text-center relative" aria-label="Zone de dépôt d'image">
196
+ <input type="file" id="imageInput" name="image" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
197
+ <div class="space-y-3">
198
+ <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
199
+ <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
200
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
201
+ 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" />
202
+ </svg>
 
 
 
 
 
 
 
 
 
 
 
 
203
  </div>
204
+ <p class="text-gray-700 font-medium">Déposez votre image ici</p>
205
+ <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
206
+ </div>
207
  </div>
208
+ <!-- Aperçu de l'image -->
209
+ <div id="imagePreview" class="hidden text-center">
210
+ <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
211
+ </div>
212
+ <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
213
+ Résoudre le problème
214
+ </button>
215
+ </form>
216
+
217
+ <!-- Loader -->
218
+ <div id="loader" class="hidden mt-8 text-center">
219
+ <span class="loader"></span>
220
+ <p class="mt-4 text-gray-600">Analyse en cours...</p>
221
+ </div>
222
+
223
+ <!-- Zone d'affichage de la solution -->
224
+ <section id="solution" class="hidden mt-8 space-y-6 relative">
225
+ <div class="border-t pt-4">
226
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
227
+ <span class="font-medium text-gray-700">Processus de Réflexion</span>
228
+ <span id="timestamp" class="timestamp"></span>
229
+ </button>
230
+ <div id="thoughtsBox" class="thought-box">
231
+ <div id="thoughtsContent" class="p-4 text-gray-600"></div>
232
+ </div>
233
  </div>
234
+ <div class="border-t pt-6">
235
+ <div class="flex justify-between items-center">
236
+ <h3 class="text-xl font-bold text-gray-800 mb-4">Solution</h3>
237
+ <!-- Bouton Sauvegarder mis en évidence -->
238
+ <button id="saveButton">Sauvegarder</button>
239
+ </div>
240
+ <div id="answerContent" class="text-gray-700 table-responsive"></div>
241
+ </div>
242
+ </section>
243
+ </main>
244
+ </div>
245
+
246
+ <!-- Modal plein écran pour les sauvegardes -->
247
+ <div id="savedModal">
248
+ <div id="savedModalContent" class="p-6">
249
+ <header class="flex justify-between items-center border-b pb-4">
250
+ <h2 class="text-2xl font-bold">Sauvegardes</h2>
251
+ <button id="closeSaved" class="text-3xl text-gray-600">&times;</button>
252
+ </header>
253
+ <div id="savedListContainer" class="mt-4">
254
+ <ul id="savedList" class="space-y-4">
255
+ <!-- Liste des sauvegardes insérée dynamiquement -->
256
+ </ul>
257
+ </div>
258
+ <div class="mt-6">
259
+ <button id="newExercise" class="blue-button w-full py-3 text-white font-medium rounded-lg">
260
+ Résoudre un nouvel exercice
261
+ </button>
262
+ </div>
263
  </div>
264
+ </div>
265
+
266
+ <script>
267
+ document.addEventListener('DOMContentLoaded', () => {
268
+ // Récupération des éléments
269
+ const form = document.getElementById('problemForm');
270
+ const imageInput = document.getElementById('imageInput');
271
+ const loader = document.getElementById('loader');
272
+ const solutionSection = document.getElementById('solution');
273
+ const thoughtsContent = document.getElementById('thoughtsContent');
274
+ const answerContent = document.getElementById('answerContent');
275
+ const thoughtsToggle = document.getElementById('thoughtsToggle');
276
+ const thoughtsBox = document.getElementById('thoughtsBox');
277
+ const imagePreview = document.getElementById('imagePreview');
278
+ const previewImage = document.getElementById('previewImage');
279
+ const timestamp = document.getElementById('timestamp');
280
+ const saveButton = document.getElementById('saveButton');
281
+ const openSaved = document.getElementById('openSaved');
282
+ const closeSaved = document.getElementById('closeSaved');
283
+ const savedModal = document.getElementById('savedModal');
284
+ const savedList = document.getElementById('savedList');
285
+ const newExercise = document.getElementById('newExercise');
286
+ const mainContent = document.getElementById('mainContent');
287
+ const tabs = document.querySelectorAll('.tab');
288
+
289
+ let startTime = null;
290
+ let timerInterval = null;
291
+ let thoughtsBuffer = '';
292
+ let answerBuffer = '';
293
+ let currentMode = null;
294
+ let updateTimeout = null;
295
+ let currentEndpoint = 'solve'; // Par défaut: 'solve' pour Gemini Pro
296
+
297
+ // Gestion des onglets pour le choix du modèle
298
+ tabs.forEach(tab => {
299
+ tab.addEventListener('click', () => {
300
+ tabs.forEach(t => t.classList.remove('active'));
301
+ tab.classList.add('active');
302
+ currentEndpoint = tab.dataset.endpoint;
303
+ });
304
+ });
305
 
306
+ // Mise à jour du temps écoulé
307
+ const updateTimestamp = () => {
308
+ if (startTime) {
309
+ const seconds = Math.floor((Date.now() - startTime) / 1000);
310
+ timestamp.textContent = `${seconds}s`;
311
+ }
312
+ };
313
+ const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
314
+ const stopTimer = () => { clearInterval(timerInterval); startTime = null; timestamp.textContent = ''; };
315
+
316
+ // Affichage de l'image sélectionnée
317
+ const handleFileSelect = file => {
318
+ if (!file) return;
319
+ const reader = new FileReader();
320
+ reader.onload = e => {
321
+ previewImage.src = e.target.result;
322
+ imagePreview.classList.remove('hidden');
323
+ };
324
+ reader.readAsDataURL(file);
325
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
+ thoughtsToggle.addEventListener('click', () => { thoughtsBox.classList.toggle('open'); });
328
+ imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
 
 
 
 
 
 
 
329
 
330
+ // Gestion du glisser-déposer
331
+ const dropZone = document.querySelector('.uploadArea');
332
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400'); });
333
+ dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); });
334
+ dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); handleFileSelect(e.dataTransfer.files[0]); });
 
 
 
335
 
336
+ // Fonction pour appliquer la coloration syntaxique
337
+ const applyHighlighting = () => {
338
+ document.querySelectorAll('pre code').forEach((block) => {
339
+ hljs.highlightBlock(block);
340
+ });
341
+ };
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
+ // Rendu MathJax et mise à jour de l'affichage
344
+ const typesetAnswerIfReady = async () => {
345
+ if (window.mathJaxReady) {
346
+ MathJax.startup.document.elements = [document.getElementById('answerContent')];
347
+ await MathJax.typesetPromise();
348
+ applyHighlighting();
349
+ answerContent.scrollTop = answerContent.scrollHeight;
350
+ } else { setTimeout(typesetAnswerIfReady, 200); }
351
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
+ const updateDisplay = async () => {
354
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
355
+ answerContent.innerHTML = marked.parse(answerBuffer);
356
+ await typesetAnswerIfReady();
357
+ updateTimeout = null;
358
+ };
359
 
360
+ const scheduleUpdate = () => { if (!updateTimeout) updateTimeout = setTimeout(updateDisplay, 200); };
361
+
362
+ marked.setOptions({
363
+ gfm: true,
364
+ breaks: true,
365
+ highlight: function(code, lang) {
366
+ if (lang && hljs.getLanguage(lang)) {
367
+ try {
368
+ return hljs.highlight(code, { language: lang }).value;
369
+ } catch (error) {}
370
+ }
371
+ return code;
372
+ }
373
+ });
374
+
375
+ // Création d'éléments pour différents types de contenu
376
+ const createContentElement = (content, type) => {
377
+ const div = document.createElement('div');
378
+ div.className = `content-${type}`;
379
+
380
+ switch(type) {
381
+ case 'text':
382
+ div.innerHTML = marked.parse(content);
383
+ break;
384
+ case 'code':
385
+ div.innerHTML = `<pre><code>${content}</code></pre>`;
386
+ break;
387
+ case 'result':
388
+ div.innerHTML = `<div class="code-execution-result">${content}</div>`;
389
+ break;
390
+ case 'image':
391
+ div.innerHTML = `<img src="data:image/png;base64,${content}" />`;
392
+ break;
393
+ default:
394
+ div.innerHTML = marked.parse(content);
395
+ }
396
+
397
+ return div;
398
+ };
399
 
400
+ // Envoi de l'image pour résolution
401
+ form.addEventListener('submit', async e => {
402
+ e.preventDefault();
403
+ const file = imageInput.files[0];
404
+ if (!file) {
405
+ Swal.fire({
406
+ icon: 'error',
407
+ title: 'Image manquante',
408
+ text: 'Veuillez sélectionner une image.'
409
+ });
410
+ return;
411
+ }
412
+
413
+ startTimer();
414
+ loader.classList.remove('hidden');
415
+ solutionSection.classList.add('hidden');
416
+ thoughtsContent.innerHTML = '';
417
+ answerContent.innerHTML = '';
418
+ thoughtsBuffer = '';
419
+ answerBuffer = '';
420
+ currentMode = null;
421
+ thoughtsBox.classList.add('open');
422
+
423
+ const formData = new FormData();
424
+ formData.append('image', file);
425
+
426
+ try {
427
+ const response = await fetch(`/${currentEndpoint}`, { method: 'POST', body: formData });
428
+ const reader = response.body.getReader();
429
+ const decoder = new TextDecoder();
430
+ let buffer = '';
431
+
432
+ const processChunk = async chunk => {
433
+ buffer += decoder.decode(chunk, { stream: true });
434
+ const lines = buffer.split('\n\n');
435
+ buffer = lines.pop();
436
+
437
+ for (const line of lines) {
438
+ if (!line.startsWith('data:')) continue;
439
+ const data = JSON.parse(line.slice(5));
440
+
441
+ if (data.mode) {
442
+ currentMode = data.mode;
443
+ loader.classList.add('hidden');
444
+ solutionSection.classList.remove('hidden');
445
+ }
446
+
447
+ if (data.content) {
448
+ // Gestion différenciée selon le type de contenu
449
+ if (data.type) {
450
+ if (currentMode === 'thinking') {
451
+ // Pour le mode thinking, on accumule tout en texte
452
+ thoughtsBuffer += data.content;
453
+ } else if (currentMode === 'answering') {
454
+ // Pour le mode answering, on traite selon le type
455
+ switch(data.type) {
456
+ case 'code':
457
+ answerBuffer += "\n```\n" + data.content + "\n```\n";
458
+ break;
459
+ case 'result':
460
+ answerBuffer += "\n> " + data.content.replace(/\n/g, "\n> ") + "\n";
461
+ break;
462
+ case 'image':
463
+ // On traitera les images séparément lors de l'affichage
464
+ answerBuffer += `\n![Résultat](data:image/png;base64,${data.content})\n`;
465
+ break;
466
+ default:
467
+ answerBuffer += data.content;
468
  }
469
+ }
470
+ } else {
471
+ // Rétrocompatibilité avec l'ancien format
472
+ if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
473
+ else if (currentMode === 'answering') { answerBuffer += data.content; }
474
  }
475
+ }
476
+
477
+ if (data.error) {
478
+ answerBuffer += `\n**Erreur:** ${data.error}\n`;
479
+ }
 
480
  }
481
+
482
+ scheduleUpdate();
483
+ };
484
+
485
+ while (true) {
486
+ const { done, value } = await reader.read();
487
+ if (done) {
488
+ if (buffer) {
 
489
  try {
490
+ const data = JSON.parse(buffer.slice(5));
491
+ if (data.content) {
492
+ if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
493
+ else if (currentMode === 'answering') { answerBuffer += data.content; }
494
+ }
495
+ } catch (e) {
496
+ console.error('Erreur lors du parsing du dernier morceau:', e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  }
498
+ }
499
+ scheduleUpdate();
500
+ break;
501
  }
502
+ await processChunk(value);
503
+ }
504
+
505
+ stopTimer();
506
+ } catch (error) {
507
+ console.error('Erreur:', error);
508
+ Swal.fire({
509
+ icon: 'error',
510
+ title: 'Erreur',
511
+ text: 'Une erreur est survenue lors du traitement de votre demande.'
512
+ });
513
+ loader.classList.add('hidden');
514
+ stopTimer();
515
+ }
516
+ });
517
+
518
+ // Sauvegarde de la solution avec SweetAlert2
519
+ saveButton.addEventListener('click', () => {
520
+ Swal.fire({
521
+ title: 'Sauvegarder la solution',
522
+ input: 'text',
523
+ inputLabel: 'Nom de la sauvegarde',
524
+ inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
525
+ showCancelButton: true,
526
+ confirmButtonText: 'Sauvegarder',
527
+ cancelButtonText: 'Annuler'
528
+ }).then((result) => {
529
+ if (result.isConfirmed && result.value) {
530
+ const saveName = result.value;
531
+ const saveData = {
532
+ answer: answerContent.innerHTML,
533
+ thinking: thoughtsContent.innerHTML,
534
+ date: new Date().toLocaleString()
535
+ };
536
+ let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
537
+ savedExercises[saveName] = saveData;
538
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
539
+ Swal.fire({
540
+ icon: 'success',
541
+ title: 'Sauvegarde réussie',
542
+ text: 'Votre solution a bien été sauvegardée !',
543
+ timer: 2000,
544
+ showConfirmButton: false
545
+ });
546
+ }
547
  });
548
+ });
549
+
550
+ // Chargement des sauvegardes dans le modal
551
+ const loadSavedList = () => {
552
+ savedList.innerHTML = '';
553
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
554
+
555
+ if (Object.keys(savedExercises).length === 0) {
556
+ savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
557
+ return;
558
+ }
559
+
560
+ for (const [name, data] of Object.entries(savedExercises)) {
561
+ const li = document.createElement('li');
562
+ li.className = 'border-b pb-2';
563
+ li.innerHTML = `
564
+ <div class="flex justify-between items-center">
565
+ <button class="text-left text-blue-600 hover:underline" data-save="${name}">
566
+ ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
567
+ </button>
568
+ <button class="text-red-500 hover:text-red-700" data-delete="${name}">
569
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
570
+ <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
571
+ </svg>
572
+ </button>
573
+ </div>
574
+ `;
575
+ savedList.appendChild(li);
576
+ }
577
+ };
578
 
579
+ // Gestion des clics sur les sauvegardes
580
+ savedList.addEventListener('click', (e) => {
581
+ // Chargement d'une sauvegarde
582
+ if (e.target && e.target.dataset.save) {
583
+ const saveName = e.target.dataset.save;
584
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
585
+ const data = savedExercises[saveName];
586
+ if (data) {
587
+ form.classList.add('hidden');
588
+ loader.classList.add('hidden');
589
+ solutionSection.classList.remove('hidden');
590
+ thoughtsContent.innerHTML = data.thinking;
591
+ answerContent.innerHTML = data.answer;
592
+ savedModal.classList.remove('active');
593
+
594
+ // Réappliquer MathJax et highlighting
595
+ MathJax.typesetPromise().then(() => {
596
+ applyHighlighting();
597
+ });
598
+ }
599
+ }
600
+
601
+ // Suppression d'une sauvegarde
602
+ if (e.target && (e.target.dataset.delete || e.target.closest('[data-delete]'))) {
603
+ const deleteName = e.target.dataset.delete || e.target.closest('[data-delete]').dataset.delete;
604
+
605
+ Swal.fire({
606
+ title: 'Êtes-vous sûr ?',
607
+ text: "Cette sauvegarde sera définitivement supprimée.",
608
+ icon: 'warning',
609
+ showCancelButton: true,
610
+ confirmButtonColor: '#3085d6',
611
+ cancelButtonColor: '#d33',
612
+ confirmButtonText: 'Oui, supprimer',
613
+ cancelButtonText: 'Annuler'
614
+ }).then((result) => {
615
+ if (result.isConfirmed) {
616
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
617
+ delete savedExercises[deleteName];
618
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
619
+
620
+ Swal.fire(
621
+ 'Supprimé !',
622
+ 'La sauvegarde a été supprimée.',
623
+ 'success'
624
+ );
625
+
626
+ loadSavedList();
627
+ }
628
+ });
629
+ }
630
+ });
631
+
632
+ // Ouverture / fermeture du modal de sauvegardes
633
+ openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
634
+ closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
635
+
636
+ // Bouton présent uniquement dans le modal pour lancer un nouvel exercice
637
+ newExercise.addEventListener('click', () => {
638
+ form.reset();
639
+ form.classList.remove('hidden');
640
+ solutionSection.classList.add('hidden');
641
+ imagePreview.classList.add('hidden');
642
+ thoughtsContent.innerHTML = '';
643
+ answerContent.innerHTML = '';
644
+ thoughtsBuffer = '';
645
+ answerBuffer = '';
646
+ savedModal.classList.remove('active');
647
+ });
648
+ });
649
+ </script>
650
  </body>
651
  </html>