Docfile commited on
Commit
81eb477
·
verified ·
1 Parent(s): c35dcb1

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +608 -570
templates/index.html CHANGED
@@ -11,30 +11,41 @@
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
- skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'] // Important pour Highlight.js
30
  },
31
  startup: {
32
- pageReady: () => { window.mathJaxReady = true; }
 
 
 
 
 
33
  }
34
  };
35
  </script>
36
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
37
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
38
 
39
  <style>
40
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
@@ -62,31 +73,35 @@
62
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
63
 
64
  .thought-box {
65
- transition: max-height 0.3s ease-out;
66
  max-height: 0;
67
  overflow: hidden;
 
 
 
 
 
 
 
 
68
  }
69
- .thought-box.open { max-height: 500px; }
70
 
71
  #thoughtsContent, #answerContent {
72
- max-height: 60vh; /* Limite la hauteur pour éviter un scroll trop long */
73
  overflow-y: auto;
74
  scroll-behavior: smooth;
75
- white-space: normal; /* Permettre le retour à la ligne normal */
76
- word-wrap: break-word; /* S'assurer que les longs mots/urls ne dépassent pas */
77
- padding: 1rem; /* Ajouter un peu de padding interne */
78
- background-color: #fff; /* Fond blanc pour la zone de réponse */
79
- border: 1px solid #e2e8f0; /* Bordure légère */
80
- border-radius: 0.375rem; /* Coins arrondis */
81
  }
82
-
83
- #thoughtsContent {
84
- background-color: #f9fafb; /* Fond légèrement différent pour les pensées */
85
  }
86
 
87
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
88
 
89
- .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
90
 
91
  table {
92
  border-collapse: collapse;
@@ -95,19 +110,20 @@
95
  border: 1px solid #d1d5db;
96
  }
97
  th, td {
98
- border: 1px solid #d1d5db;
99
- padding: 0.5rem;
100
  text-align: left;
101
  }
102
  th { background-color: #f3f4f6; font-weight: 600; }
103
- .table-responsive { overflow-x: auto; margin: 1rem 0; } /* Ajouter un margin */
104
 
105
  #saveButton {
106
- background: #10b981; /* Vert pour sauvegarder */
107
  color: white;
108
  padding: 0.5rem 1rem;
109
  border-radius: 0.375rem;
110
  transition: background-color 0.2s ease;
 
111
  }
112
  #saveButton:hover { background: #059669; }
113
 
@@ -115,168 +131,129 @@
115
  display: none;
116
  position: fixed;
117
  inset: 0;
118
- background: rgba(0,0,0,0.5);
119
  z-index: 50;
 
120
  }
121
- #savedModal.active { display: flex; align-items: center; justify-content: center;} /* Centrer le modal */
122
  #savedModalContent {
123
  background: #fff;
124
  width: 90%;
125
- max-width: 800px; /* Limiter la largeur max */
126
- max-height: 90vh; /* Limiter la hauteur max */
127
- overflow-y: auto;
128
- border-radius: 0.5rem; /* Coins arrondis pour le modal */
129
  display: flex;
130
  flex-direction: column;
131
  }
132
- #savedModalContent header {
133
- padding: 1rem 1.5rem;
134
- }
135
- #savedListContainer {
136
- padding: 0 1.5rem 1rem; /* Padding pour la liste */
137
- flex-grow: 1; /* Permet à la liste de prendre l'espace */
138
- overflow-y: auto; /* Scroll interne si besoin */
139
- }
140
- #savedModalContent .modal-footer {
141
- padding: 1rem 1.5rem;
142
- border-top: 1px solid #e2e8f0;
143
- text-align: right;
144
- }
145
-
146
- /* Styles spécifiques pour le code et son exécution */
147
- pre {
148
- background-color: #f8f8f8;
149
- border: 1px solid #e2e8f0;
150
- border-radius: 0.375rem;
151
- padding: 1rem;
152
- margin: 1rem 0; /* Espace autour des blocs de code */
153
  overflow-x: auto;
154
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
155
- white-space: pre; /* Conserver les espaces et retours ligne du code */
 
 
 
156
  }
157
 
158
- code {
159
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
160
- background-color: rgba(209, 213, 219, 0.3); /* Léger fond pour code inline */
161
- padding: 0.1em 0.3em;
162
- border-radius: 0.25rem;
 
 
 
 
 
163
  }
164
-
165
- pre code {
166
- background-color: transparent; /* Pas de fond spécial dans pre */
167
- padding: 0;
168
- border-radius: 0;
169
  }
170
 
171
- .code-execution-result {
172
- background-color: #f0fff4; /* Fond vert pâle pour résultat */
173
- border-left: 4px solid #48bb78;
174
- padding: 1rem;
175
- margin: 1rem 0; /* Espace autour */
176
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
177
- white-space: pre-wrap; /* Permettre retour à la ligne */
178
- overflow-x: auto; /* Scroll horizontal si nécessaire */
179
- }
180
 
181
- /* Styles pour les types de contenu spécifiques */
182
- .content-text p:last-child { margin-bottom: 0; } /* Éviter double marge */
183
- .content-code {}
184
- .content-result {}
185
- .content-image { margin: 1rem 0; text-align: center; }
186
- .content-image img {
187
- max-width: 100%;
188
- height: auto; /* Maintenir proportions */
189
- display: inline-block; /* Pour que text-align fonctionne */
190
- border: 1px solid #d1d5db; /* Légère bordure autour de l'image */
191
- border-radius: 0.25rem;
192
  }
193
 
194
- /* Onglets pour choisir le modèle */
195
  .tabs {
196
  display: flex;
197
  border-bottom: 1px solid #e2e8f0;
198
- margin-bottom: 1rem;
199
  }
200
 
201
  .tab {
202
- padding: 0.5rem 1rem;
203
  cursor: pointer;
204
- border-bottom: 2px solid transparent;
205
- color: #6b7280; /* Gris par défaut */
206
- transition: color 0.2s ease, border-color 0.2s ease;
 
207
  }
 
208
  .tab:hover {
209
- color: #3b82f6;
210
  }
211
 
212
  .tab.active {
213
- border-bottom: 2px solid #3b82f6;
214
- color: #3b82f6;
215
  font-weight: 600;
216
  }
217
-
218
- .delete-button {
219
- color: #ef4444; /* Rouge pour supprimer */
220
- background: none;
221
- border: none;
222
- padding: 0.25rem;
223
- cursor: pointer;
224
- transition: color 0.2s ease;
225
- }
226
- .delete-button:hover {
227
- color: #dc2626;
228
- }
229
- .delete-button svg {
230
- display: inline-block; /* Empêche les problèmes de layout */
231
- }
232
  </style>
233
  </head>
234
- <body class="bg-gray-100 p-4 min-h-screen flex flex-col">
235
- <div class="max-w-4xl mx-auto w-full bg-white shadow-md rounded-lg p-6 flex-grow flex flex-col">
236
- <header class="text-center mb-8">
237
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
238
- <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
239
  <div class="mt-4 flex justify-end">
240
- <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">
241
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1" viewBox="0 0 20 20" fill="currentColor">
242
- <path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-3.03L5 18V4z" />
243
- </svg>
244
- Sauvegardes
245
  </button>
246
  </div>
247
  </header>
248
 
249
- <main id="mainContent" class="flex-grow flex flex-col">
250
- <form id="problemForm" class="space-y-6 flex-grow flex flex-col" novalidate>
251
- <!-- Onglets pour choisir le modèle -->
252
  <div class="tabs">
253
- <div class="tab active" data-endpoint="solve">Gemini Pro</div>
254
- <div class="tab" data-endpoint="solved">Gemini Flash</div>
255
  </div>
256
 
257
- <!-- Zone de dépôt / sélection d'image -->
258
- <div class="uploadArea p-8 text-center relative cursor-pointer flex-grow flex flex-col justify-center items-center" aria-label="Zone de dépôt d'image">
259
  <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">
260
  <div class="space-y-3">
261
- <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center text-blue-400">
262
- <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
263
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
264
  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" />
265
  </svg>
266
  </div>
267
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
268
- <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
269
- </div>
270
- <!-- Aperçu de l'image -->
271
- <div id="imagePreview" class="hidden mt-4">
272
- <img id="previewImage" class="preview-image mx-auto border border-gray-300 rounded" alt="Prévisualisation de l'image">
273
  </div>
274
  </div>
275
-
276
- <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg flex items-center justify-center">
277
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
278
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
279
- </svg>
280
  Résoudre le problème
281
  </button>
282
  </form>
@@ -284,73 +261,97 @@
284
  <!-- Loader -->
285
  <div id="loader" class="hidden mt-8 text-center">
286
  <span class="loader"></span>
287
- <p class="mt-4 text-gray-600">Analyse en cours...</p>
288
  </div>
289
 
290
- <!-- Zone d'affichage de la solution -->
291
- <section id="solution" class="hidden mt-8 space-y-6 flex-grow flex flex-col">
292
-
293
- <!-- Zone Pensées -->
294
- <div class="border rounded-lg overflow-hidden shadow-sm">
295
- <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-50 border-b">
296
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
297
- <div class="flex items-center">
298
- <span id="timestamp" class="timestamp mr-2"></span>
299
- <svg id="toggleIcon" class="w-5 h-5 text-gray-500 transform transition-transform duration-200" viewBox="0 0 20 20" fill="currentColor">
300
- <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
301
- </svg>
302
- </div>
 
303
  </button>
304
- <div id="thoughtsBox" class="thought-box bg-gray-50">
305
- <div id="thoughtsContent" class="text-gray-600 text-sm"></div> <!-- #thoughtsContent est DANS #thoughtsBox -->
306
- </div>
307
  </div>
308
 
309
- <!-- Zone Solution -->
310
- <div class="border rounded-lg shadow-sm flex-grow flex flex-col">
311
  <div class="flex justify-between items-center p-3 border-b bg-white">
312
- <h3 class="text-xl font-bold text-gray-800">Solution</h3>
313
- <button id="saveButton" title="Sauvegarder cette solution">
314
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1" viewBox="0 0 20 20" fill="currentColor">
315
- <path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-3.03L5 18V4z" />
316
- </svg>
317
- Sauvegarder
318
- </button>
319
  </div>
320
- <div id="answerContent" class="text-gray-700 flex-grow bg-white rounded-b-lg"></div> <!-- #answerContent est DANS la div Solution -->
321
  </div>
322
-
323
  </section>
324
  </main>
325
  </div>
326
 
327
- <!-- Modal plein écran pour les sauvegardes -->
328
  <div id="savedModal">
329
- <div id="savedModalContent" class="p-0"> <!-- Padding géré par les sections internes -->
330
- <header class="flex justify-between items-center border-b pb-4 p-4">
331
- <h2 class="text-2xl font-bold text-gray-800">Sauvegardes</h2>
332
  <button id="closeSaved" class="text-3xl text-gray-500 hover:text-gray-700">×</button>
333
  </header>
334
- <div id="savedListContainer" class="mt-4 flex-grow overflow-y-auto px-4">
335
  <ul id="savedList" class="space-y-4">
336
- <!-- Liste des sauvegardes insérée dynamiquement -->
337
- <li class="text-gray-500 text-center py-8">Chargement...</li>
338
  </ul>
339
  </div>
340
- <div class="modal-footer mt-6 p-4 bg-gray-50 border-t">
341
- <button id="newExercise" class="blue-button px-4 py-2 text-white font-medium rounded-lg">
342
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1" viewBox="0 0 20 20" fill="currentColor">
343
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
344
- </svg>
345
- Nouvel Exercice
346
  </button>
347
- </div>
348
  </div>
349
  </div>
350
 
351
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  document.addEventListener('DOMContentLoaded', () => {
353
- // --- Récupération des éléments ---
354
  const form = document.getElementById('problemForm');
355
  const imageInput = document.getElementById('imageInput');
356
  const loader = document.getElementById('loader');
@@ -369,61 +370,44 @@
369
  const savedModal = document.getElementById('savedModal');
370
  const savedList = document.getElementById('savedList');
371
  const newExercise = document.getElementById('newExercise');
372
- const mainContent = document.getElementById('mainContent');
373
  const tabs = document.querySelectorAll('.tab');
374
- const dropZone = document.querySelector('.uploadArea'); // Récupérer la drop zone
375
 
376
- // --- États et Variables ---
377
  let startTime = null;
378
  let timerInterval = null;
379
- let thoughtsBuffer = ''; // Garder un buffer pour les pensées
380
- let currentMode = null;
381
- let currentEndpoint = 'solve'; // Par défaut: 'solve' pour Gemini Pro
382
- let renderTimeout; // Pour le debouncing
383
-
384
- // --- Configuration Marked ---
385
- marked.setOptions({
386
- gfm: true,
387
- breaks: true,
388
- highlight: function (code, lang) {
389
- const language = hljs.getLanguage(lang) ? lang : 'plaintext';
390
- try {
391
- // Structure attendue par marked avec hljs
392
- return hljs.highlight(code, { language, ignoreIllegals: true }).value;
393
- } catch (error) {
394
- console.error("Highlighting error:", error);
395
- // Retour en plain text en cas d'erreur
396
- return hljs.highlight(code, { language: 'plaintext', ignoreIllegals: true }).value;
397
- }
398
- }
399
- });
400
 
401
- // --- Gestion des Onglets Modèle ---
402
- tabs.forEach(tab => {
403
- tab.addEventListener('click', () => {
404
- tabs.forEach(t => t.classList.remove('active'));
405
- tab.classList.add('active');
406
- currentEndpoint = tab.dataset.endpoint;
407
- });
408
- });
409
 
410
- // --- Gestion du Timer ---
411
  const updateTimestamp = () => {
412
  if (startTime) {
413
  const seconds = Math.floor((Date.now() - startTime) / 1000);
414
- timestamp.textContent = `${seconds}s`;
 
 
415
  }
416
  };
417
- const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
418
- const stopTimer = () => { clearInterval(timerInterval); updateTimestamp(); /* Met à jour une dernière fois */ };
419
 
420
- // --- Gestion Fichiers & Prévisualisation ---
421
  const handleFileSelect = file => {
422
  if (!file || !file.type.startsWith('image/')) {
423
- Swal.fire('Fichier invalide', 'Veuillez sélectionner un fichier image.', 'warning');
424
- imagePreview.classList.add('hidden'); // Cacher si invalide
425
- imageInput.value = ''; // Réinitialiser l'input file
426
- return null; // Indiquer qu'aucun fichier valide n'a été sélectionné
 
 
 
427
  }
428
  const reader = new FileReader();
429
  reader.onload = e => {
@@ -431,430 +415,484 @@
431
  imagePreview.classList.remove('hidden');
432
  };
433
  reader.readAsDataURL(file);
434
- return file; // Retourner le fichier valide
435
  };
436
 
437
- imageInput.addEventListener('change', e => {
438
- if (e.target.files.length > 0) {
439
- handleFileSelect(e.target.files[0]);
440
- } else {
441
- imagePreview.classList.add('hidden');
442
- }
443
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
- // --- Gestion Glisser-Déposer ---
446
- dropZone.addEventListener('click', () => imageInput.click()); // Rendre toute la zone cliquable
447
- dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400', 'bg-blue-50'); });
448
- dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400', 'bg-blue-50'); });
449
- dropZone.addEventListener('drop', e => {
450
- e.preventDefault();
451
- dropZone.classList.remove('border-blue-400', 'bg-blue-50');
452
- if (e.dataTransfer.files.length > 0) {
453
- const file = handleFileSelect(e.dataTransfer.files[0]);
454
- if (file) {
455
- // Mettre à jour l'input file pour que le formulaire le trouve
456
- const dataTransfer = new DataTransfer();
457
- dataTransfer.items.add(file);
458
- imageInput.files = dataTransfer.files;
459
- }
460
- }
461
- });
462
 
463
- // --- Affichage/Masquage Pensées ---
464
- thoughtsToggle.addEventListener('click', () => {
465
- thoughtsBox.classList.toggle('open');
466
- toggleIcon.classList.toggle('rotate-180'); // Pivoter l'icône chevron
467
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
- // --- Debouncer pour Rendu MathJax/Highlight ---
470
- const debouncedRender = () => {
471
- clearTimeout(renderTimeout);
472
- renderTimeout = setTimeout(async () => {
473
- // Highlightjs d'abord, car MathJax peut interférer avec les classes hljs
474
- document.querySelectorAll('#answerContent pre code:not(.hljs)').forEach((block) => {
475
- try {
476
- hljs.highlightElement(block);
477
- } catch (e) { console.error("Highlighting error on block:", e, block); }
478
- });
479
-
480
- // Puis MathJax
481
- if (window.mathJaxReady && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
482
- console.log("Typesetting MathJax...");
483
- await MathJax.typesetPromise([answerContent]).catch(err => console.error('MathJax Typesetting Error:', err));
484
- console.log("MathJax Typesetting Done.");
485
- } else {
486
- console.log("MathJax not ready or typesetPromise not available");
487
- }
488
- // S'assurer que le scroll est bien en bas après le rendu
489
- answerContent.scrollTop = answerContent.scrollHeight;
490
-
491
- }, 300); // Délai de 300ms
492
  };
493
 
494
- // --- Soumission du Formulaire et Traitement SSE ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  form.addEventListener('submit', async e => {
496
  e.preventDefault();
497
  const file = imageInput.files[0];
498
  if (!file) {
499
- Swal.fire({ icon: 'error', title: 'Image manquante', text: 'Veuillez sélectionner une image valide.' });
500
  return;
501
  }
 
 
502
 
503
- // Réinitialisation de l'interface
504
  startTimer();
505
  loader.classList.remove('hidden');
506
  solutionSection.classList.add('hidden');
507
- form.classList.add('hidden'); // Cacher le formulaire pendant le traitement
508
  thoughtsContent.innerHTML = '';
509
- answerContent.innerHTML = ''; // Vider le contenu précédent
510
  thoughtsBuffer = '';
 
511
  currentMode = null;
512
- thoughtsBox.classList.remove('open'); // Fermer les pensées par défaut
513
- toggleIcon.classList.remove('rotate-180');
 
 
 
 
 
514
 
 
515
  const formData = new FormData();
516
  formData.append('image', file);
517
 
518
- try {
519
- const response = await fetch(`/${currentEndpoint}`, { method: 'POST', body: formData });
520
 
521
- if (!response.ok || !response.body) {
522
- throw new Error(`Erreur serveur: ${response.status} ${response.statusText}`);
523
- }
524
-
525
- const reader = response.body.getReader();
526
- const decoder = new TextDecoder();
527
- buffer = ''; // Buffer pour les données SSE incomplètes
528
-
529
- // Fonction récursive pour lire le stream
530
- const processText = async ({ done, value }) => {
531
- if (done) {
532
- console.log("Stream terminé.");
533
- stopTimer();
534
- loader.classList.add('hidden');
535
- solutionSection.classList.remove('hidden'); // Afficher la section solution
536
- // Appel final de rendu pour s'assurer que tout est traité
537
- debouncedRender();
538
- return;
539
- }
540
-
541
- buffer += decoder.decode(value, { stream: true });
542
- const lines = buffer.split('\n\n');
543
- buffer = lines.pop(); // Garder la dernière ligne potentiellement incomplète
544
-
545
- for (const line of lines) {
546
- if (!line.startsWith('data:')) continue;
547
-
548
- try {
549
- const data = JSON.parse(line.slice(5));
550
-
551
- if (data.mode) {
552
- currentMode = data.mode;
553
- if (currentMode === 'thinking' && !thoughtsBox.classList.contains('open')) {
554
- thoughtsBox.classList.add('open'); // Ouvrir automatiquement si pensée détectée
555
- toggleIcon.classList.add('rotate-180');
556
- }
557
- // Afficher la section solution dès qu'on commence à répondre
558
- if (currentMode === 'answering' && solutionSection.classList.contains('hidden')) {
559
- loader.classList.add('hidden');
560
- solutionSection.classList.remove('hidden');
561
- }
562
- }
563
-
564
- if (data.error) {
565
- const errorEl = document.createElement('div');
566
- errorEl.className = 'my-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded';
567
- errorEl.textContent = `Erreur Stream: ${data.error}`;
568
- answerContent.appendChild(errorEl);
569
- answerContent.scrollTop = answerContent.scrollHeight;
570
- continue; // Passer au prochain message
571
- }
572
-
573
- if (data.content) {
574
- if (currentMode === 'thinking') {
575
- // Mettre à jour le buffer et l'affichage des pensées
576
- thoughtsBuffer += data.content;
577
- // On peut décommenter la ligne suivante si on veut un rendu live des pensées (peut impacter perf)
578
- // thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
579
- // Ou juste ajouter le texte brut
580
- thoughtsContent.textContent += data.content; // Plus simple, moins de parsing
581
- thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
582
-
583
- } else if (currentMode === 'answering' && data.type) {
584
- let el;
585
- switch (data.type) {
586
- case 'text':
587
- el = document.createElement('div');
588
- el.className = 'content-text';
589
- // Parse seulement ce bout de texte
590
- el.innerHTML = marked.parse(data.content);
591
- // Append intelligemment pour éviter trop de <p> vides si on reçoit juste "$...$"
592
- if (el.firstChild && el.firstChild.nodeName === 'P' && !el.firstChild.nextSibling && el.firstChild.innerHTML.trim() === data.content.trim()) {
593
- // Si le contenu parsé est juste le contenu original dans un <p>, on ajoute le contenu brut (MathJax le prendra)
594
- answerContent.append(document.createTextNode(data.content));
595
- } else {
596
- // Sinon, on ajoute le HTML parsé
597
- answerContent.appendChild(el);
598
- }
599
- break;
600
- case 'code':
601
- // Utiliser marked pour créer le <pre><code> avec la bonne classe pour hljs
602
- el = document.createElement('div');
603
- el.className = 'content-code';
604
- el.innerHTML = marked.parse('```' + (data.lang || '') + '\n' + data.content + '\n```');
605
- answerContent.appendChild(el.firstChild); // Ajouter directement le <pre> généré par marked
606
- break;
607
- case 'result':
608
- el = document.createElement('pre'); // Utiliser <pre> pour préserver le formatage
609
- el.className = 'code-execution-result';
610
- el.textContent = data.content;
611
- answerContent.appendChild(el);
612
- break;
613
- case 'image':
614
- el = document.createElement('div');
615
- el.className = 'content-image'; // Classe CSS pour le style
616
- el.innerHTML = `<img src="data:image/png;base64,${data.content}" alt="Generated image" class="inline-block max-w-full h-auto border rounded">`; // Styles inline/tailwind
617
- answerContent.appendChild(el);
618
- break;
619
- default:
620
- console.warn("Type de contenu inconnu reçu:", data.type, data.content);
621
- // Ajouter comme texte simple en fallback
622
- el = document.createElement('span');
623
- el.textContent = data.content;
624
- answerContent.appendChild(el);
625
- }
626
- // Scroller et demander un rendu
627
- answerContent.scrollTop = answerContent.scrollHeight;
628
- debouncedRender();
629
- }
630
- }
631
- } catch (e) {
632
- console.error("Erreur parsing JSON du stream:", e, "Ligne:", line);
633
- }
634
- }
635
- // Lire le chunk suivant
636
- reader.read().then(processText).catch(streamError => {
637
- console.error("Erreur de lecture du stream:", streamError);
638
- stopTimer();
639
- loader.classList.add('hidden');
640
- // Afficher l'erreur dans la zone de réponse
641
- const errorEl = document.createElement('div');
642
- errorEl.className = 'my-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded';
643
- errorEl.textContent = `Erreur de lecture: ${streamError.message}`;
644
- answerContent.appendChild(errorEl);
645
- solutionSection.classList.remove('hidden'); // Afficher la section pour voir l'erreur
646
- });
647
- };
648
-
649
- // Démarrer la lecture du stream
650
- reader.read().then(processText).catch(initialReadError => {
651
- console.error("Erreur initiale de lecture du stream:", initialReadError);
652
- stopTimer();
653
- loader.classList.add('hidden');
654
- Swal.fire('Erreur Réseau', 'Impossible de démarrer la lecture de la réponse du serveur.', 'error');
655
- form.classList.remove('hidden'); // Ré-afficher le formulaire si erreur initiale
656
- });
657
 
658
- } catch (error) {
659
- console.error('Erreur fetch ou setup:', error);
660
- Swal.fire({ icon: 'error', title: 'Erreur', text: `Une erreur est survenue: ${error.message}` });
661
- loader.classList.add('hidden');
662
- form.classList.remove('hidden'); // Ré-afficher le formulaire en cas d'erreur
663
- stopTimer();
664
- }
665
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
- // --- Gestion Sauvegardes ---
668
- const SAVED_EXERCISES_KEY = 'savedExercises_mariam_m1';
669
 
670
- // Sauvegarde
671
- saveButton.addEventListener('click', () => {
672
- // Finaliser le rendu avant sauvegarde
673
- if (thoughtsBuffer) {
674
- thoughtsContent.innerHTML = marked.parse(thoughtsBuffer); // Assurer que les pensées sont parsées
675
- }
676
- // Appeler debouncedRender une dernière fois et attendre un peu
677
- debouncedRender();
678
- setTimeout(() => {
679
- Swal.fire({
680
- title: 'Sauvegarder la solution',
681
- input: 'text',
682
- inputLabel: 'Nom de la sauvegarde',
683
- inputPlaceholder: 'Ex: Exercice Trigonométrie Ch. 5',
684
- showCancelButton: true,
685
- confirmButtonText: 'Sauvegarder',
686
- cancelButtonText: 'Annuler',
687
- inputValidator: (value) => {
688
- if (!value) {
689
- return 'Veuillez entrer un nom !'
690
  }
691
- }
692
- }).then((result) => {
693
- if (result.isConfirmed && result.value) {
694
- const saveName = result.value;
695
- const saveData = {
696
- name: saveName, // Stocker le nom aussi dans l'objet
697
- answer: answerContent.innerHTML, // Sauvegarder le HTML rendu
698
- thinking: thoughtsContent.innerHTML, // Sauvegarder le HTML rendu
699
- date: new Date().toISOString() // Format ISO pour tri plus facile
700
- };
701
- let savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
702
- // Vérifier si un nom existe déjà (optionnel: proposer remplacement)
703
- const existingIndex = savedExercises.findIndex(item => item.name === saveName);
704
- if (existingIndex > -1) {
705
- // Remplacer l'ancienne sauvegarde
706
- savedExercises[existingIndex] = saveData;
707
- } else {
708
- savedExercises.push(saveData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  }
710
- // Trier par date (plus récent en premier)
711
- savedExercises.sort((a, b) => new Date(b.date) - new Date(a.date));
712
-
713
- localStorage.setItem(SAVED_EXERCISES_KEY, JSON.stringify(savedExercises));
714
- Swal.fire({
715
- icon: 'success',
716
- title: 'Sauvegarde réussie!',
717
- timer: 1500,
718
- showConfirmButton: false
719
- });
720
- }
721
- });
722
- }, 500); // Attendre 500ms que le rendu final soit probablement terminé
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
  });
724
 
725
- // Chargement de la liste
726
  const loadSavedList = () => {
727
- savedList.innerHTML = ''; // Vider la liste actuelle
728
- const savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
 
 
 
 
 
 
729
 
730
- if (savedExercises.length === 0) {
 
731
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
732
  return;
733
  }
734
 
735
- savedExercises.forEach(data => {
 
736
  const li = document.createElement('li');
737
- li.className = 'border-b pb-3 mb-3 flex justify-between items-center group'; // Ajouter group pour hover sur bouton delete
738
-
739
- const contentDiv = document.createElement('div');
740
- contentDiv.className = 'flex-grow mr-4';
741
-
742
- const loadButton = document.createElement('button');
743
- loadButton.className = 'text-left text-blue-600 hover:underline focus:outline-none';
744
- loadButton.dataset.saveName = data.name;
745
- loadButton.innerHTML = `
746
- <span class="font-medium">${data.name}</span>
747
- <span class="block text-gray-500 text-xs">${new Date(data.date).toLocaleString('fr-FR')}</span>
748
- `;
749
- contentDiv.appendChild(loadButton);
750
-
751
- const deleteButton = document.createElement('button');
752
- deleteButton.className = 'delete-button flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
753
- deleteButton.dataset.deleteName = data.name;
754
- deleteButton.title = "Supprimer cette sauvegarde";
755
- deleteButton.innerHTML = `
756
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
757
- <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" />
758
- </svg>
759
  `;
760
-
761
- li.appendChild(contentDiv);
762
- li.appendChild(deleteButton);
763
  savedList.appendChild(li);
764
  });
765
  };
766
 
767
- // Gestion des clics dans la liste (chargement/suppression)
768
  savedList.addEventListener('click', (e) => {
769
- const loadBtn = e.target.closest('[data-save-name]');
770
- const deleteBtn = e.target.closest('[data-delete-name]');
771
-
772
- if (loadBtn) {
773
- const saveName = loadBtn.dataset.saveName;
774
- const savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
775
- const data = savedExercises.find(item => item.name === saveName);
 
776
  if (data) {
777
- form.classList.add('hidden');
778
  loader.classList.add('hidden');
779
- solutionSection.classList.remove('hidden');
780
- thoughtsContent.innerHTML = data.thinking; // Restaurer le HTML
781
- answerContent.innerHTML = data.answer; // Restaurer le HTML
782
- savedModal.classList.remove('active');
783
- stopTimer(); // Arrêter le timer si on charge une sauvegarde
784
- timestamp.textContent = ''; // Effacer le temps écoulé
785
-
786
- // Forcer le rendu MathJax/Highlight sur le contenu chargé
787
- setTimeout(() => { // Donner le temps au DOM de se mettre à jour
788
- if (data.thinking) {
789
- thoughtsBox.classList.add('open');
790
- toggleIcon.classList.add('rotate-180');
791
- } else {
792
- thoughtsBox.classList.remove('open');
793
- toggleIcon.classList.remove('rotate-180');
794
- }
795
- debouncedRender();
796
- }, 100);
 
797
  }
798
- } else if (deleteBtn) {
799
- const deleteName = deleteBtn.dataset.deleteName;
 
 
 
800
  Swal.fire({
801
  title: 'Êtes-vous sûr ?',
802
- text: `Supprimer définitivement la sauvegarde "${deleteName}" ?`,
803
  icon: 'warning',
804
  showCancelButton: true,
805
- confirmButtonColor: '#d33', // Rouge pour confirmer la suppression
806
  cancelButtonColor: '#3085d6',
807
- confirmButtonText: 'Oui, supprimer',
808
  cancelButtonText: 'Annuler'
809
  }).then((result) => {
810
  if (result.isConfirmed) {
811
- let savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
812
- savedExercises = savedExercises.filter(item => item.name !== deleteName);
813
- localStorage.setItem(SAVED_EXERCISES_KEY, JSON.stringify(savedExercises));
814
- Swal.fire( 'Supprimé !', 'La sauvegarde a été supprimée.', 'success' );
815
- loadSavedList(); // Recharger la liste
816
  }
817
  });
818
  }
819
  });
820
 
821
- // Ouverture / fermeture du modal de sauvegardes
822
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
823
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
824
- // Fermer le modal si on clique en dehors du contenu
825
- savedModal.addEventListener('click', (e) => {
826
- if (e.target === savedModal) {
827
  savedModal.classList.remove('active');
828
  }
829
- });
830
 
831
 
832
- // Bouton "Nouvel Exercice"
833
- const resetApp = () => {
834
- form.reset(); // Réinitialiser les champs du formulaire
835
- form.classList.remove('hidden');
836
- solutionSection.classList.add('hidden');
837
- loader.classList.add('hidden');
838
- imagePreview.classList.add('hidden'); // Cacher la prévisualisation
839
- previewImage.src = ''; // Vider la source de l'image
840
- imageInput.value = ''; // Important pour pouvoir re-sélectionner le même fichier
841
  thoughtsContent.innerHTML = '';
842
  answerContent.innerHTML = '';
843
  thoughtsBuffer = '';
844
- currentMode = null;
845
- stopTimer();
846
- timestamp.textContent = '';
847
- thoughtsBox.classList.remove('open');
848
- toggleIcon.classList.remove('rotate-180');
849
- savedModal.classList.remove('active'); // Fermer le modal si ouvert
850
- }
851
-
852
- newExercise.addEventListener('click', resetApp);
853
 
854
- // --- Initialisation ---
855
- resetApp(); // Assurer un état propre au chargement
 
 
 
 
 
856
 
857
- });
858
  </script>
859
  </body>
860
  </html>
 
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.9.0/styles/github.min.css">
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
16
+ <!-- Include Python language if needed -->
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
18
 
19
+
20
+ <!-- Marked.js for Markdown parsing -->
21
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
22
+
23
+ <!-- MathJax Configuration -->
24
  <script>
25
  window.MathJax = {
26
  tex: {
27
  inlineMath: [['$', '$']],
28
  displayMath: [['$$', '$$']],
29
  processEscapes: true,
30
+ packages: {'[+]': ['autoload', 'ams']} // Using autoload package
31
  },
32
  options: {
33
  enableMenu: false,
34
  messageStyle: 'none',
35
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'] // Ensure MathJax skips code blocks
36
  },
37
  startup: {
38
+ // Use async ready function to ensure MathJax is fully loaded
39
+ ready: () => {
40
+ console.log('MathJax is ready');
41
+ MathJax.startup.defaultReady();
42
+ window.mathJaxReady = true; // Set flag when ready
43
+ }
44
  }
45
  };
46
  </script>
47
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
48
+
49
 
50
  <style>
51
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
 
73
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
74
 
75
  .thought-box {
76
+ transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
77
  max-height: 0;
78
  overflow: hidden;
79
+ opacity: 0;
80
+ border: 1px solid #e5e7eb; /* Add border for clarity */
81
+ border-radius: 0.375rem; /* Rounded corners */
82
+ margin-top: 0.5rem; /* Spacing */
83
+ }
84
+ .thought-box.open {
85
+ max-height: 500px; /* Or a larger value if needed */
86
+ opacity: 1;
87
  }
 
88
 
89
  #thoughtsContent, #answerContent {
90
+ max-height: 60vh; /* Use viewport height for better scaling */
91
  overflow-y: auto;
92
  scroll-behavior: smooth;
93
+ white-space: pre-wrap; /* Allow text wrapping */
94
+ word-wrap: break-word; /* Break long words */
95
+ background-color: #f9fafb; /* Slight background tint */
96
+ padding: 1rem; /* Padding inside content areas */
 
 
97
  }
98
+ #answerContent {
99
+ background-color: white; /* Keep answer content white */
 
100
  }
101
 
102
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
103
 
104
+ .timestamp { color: #6b7280; font-size: 0.8em; margin-left: 8px; }
105
 
106
  table {
107
  border-collapse: collapse;
 
110
  border: 1px solid #d1d5db;
111
  }
112
  th, td {
113
+ border: 1px solid #e5e7eb; /* Lighter border */
114
+ padding: 0.75rem; /* More padding */
115
  text-align: left;
116
  }
117
  th { background-color: #f3f4f6; font-weight: 600; }
118
+ .table-responsive { overflow-x: auto; }
119
 
120
  #saveButton {
121
+ background: #10b981; /* Green button */
122
  color: white;
123
  padding: 0.5rem 1rem;
124
  border-radius: 0.375rem;
125
  transition: background-color 0.2s ease;
126
+ font-weight: 500;
127
  }
128
  #saveButton:hover { background: #059669; }
129
 
 
131
  display: none;
132
  position: fixed;
133
  inset: 0;
134
+ background: rgba(0,0,0,0.6); /* Darker overlay */
135
  z-index: 50;
136
+ overflow-y: auto; /* Allow modal scroll */
137
  }
138
+ #savedModal.active { display: flex; align-items: center; justify-content: center; }
139
  #savedModalContent {
140
  background: #fff;
141
  width: 90%;
142
+ max-width: 800px; /* Limit max width */
143
+ max-height: 90vh; /* Limit max height */
144
+ border-radius: 0.5rem;
145
+ overflow: hidden; /* Hide inner overflow initially */
146
  display: flex;
147
  flex-direction: column;
148
  }
149
+ #savedModalContent header { padding: 1.5rem; border-bottom: 1px solid #e5e7eb; }
150
+ #savedModalContent #savedListContainer { padding: 1.5rem; overflow-y: auto; flex-grow: 1; }
151
+ #savedModalContent footer { padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; background-color: #f9fafb; }
152
+
153
+
154
+ /* Styles specific to code blocks generated by marked.js and highlighted by highlight.js */
155
+ #answerContent pre code.hljs { /* Target highlighted code blocks */
156
+ display: block;
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  overflow-x: auto;
158
+ padding: 1em;
159
+ background: #f3f4f6; /* Background matching Tailwind gray */
160
+ color: #1f2937; /* Dark text */
161
+ border-radius: 0.375rem;
162
+ border: 1px solid #e5e7eb;
163
  }
164
 
165
+ /* Style for blockquotes used for code execution results */
166
+ #answerContent blockquote {
167
+ background-color: #eff6ff; /* Lighter blue background */
168
+ border-left: 4px solid #3b82f6; /* Blue left border */
169
+ padding: 0.75rem 1rem;
170
+ margin: 1rem 0;
171
+ color: #1e40af; /* Darker blue text */
172
+ white-space: pre-wrap; /* Preserve line breaks and spaces */
173
+ word-wrap: break-word; /* Break long lines */
174
+ font-style: italic; /* Italicize results */
175
  }
176
+ #answerContent blockquote p {
177
+ margin: 0; /* Remove default paragraph margins inside blockquote */
 
 
 
178
  }
179
 
 
 
 
 
 
 
 
 
 
180
 
181
+ /* Styles for images within the answer */
182
+ #answerContent img {
183
+ max-width: 100%;
184
+ height: auto; /* Maintain aspect ratio */
185
+ display: block; /* Prevent extra space below image */
186
+ margin: 1rem auto; /* Center images with margin */
187
+ border: 1px solid #d1d5db; /* Optional border */
188
+ border-radius: 0.25rem; /* Optional rounded corners */
 
 
 
189
  }
190
 
191
+ /* Tabs styling */
192
  .tabs {
193
  display: flex;
194
  border-bottom: 1px solid #e2e8f0;
195
+ margin-bottom: 1.5rem; /* More space below tabs */
196
  }
197
 
198
  .tab {
199
+ padding: 0.75rem 1.5rem; /* More padding */
200
  cursor: pointer;
201
+ border-bottom: 3px solid transparent;
202
+ color: #4b5563; /* Gray text */
203
+ font-weight: 500;
204
+ transition: border-color 0.2s ease, color 0.2s ease;
205
  }
206
+
207
  .tab:hover {
208
+ color: #1f2937; /* Darker text on hover */
209
  }
210
 
211
  .tab.active {
212
+ border-bottom: 3px solid #3b82f6;
213
+ color: #3b82f6; /* Blue text */
214
  font-weight: 600;
215
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  </style>
217
  </head>
218
+ <body class="bg-gray-50 p-4 min-h-screen flex items-center justify-center">
219
+ <div class="max-w-4xl mx-auto w-full bg-white p-8 rounded-lg shadow-md">
220
+ <header class="p-6 text-center mb-6 border-b border-gray-200">
221
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
222
+ <p class="text-gray-600 mt-2">Solution Mathématique/Physique/Chimie Intelligente</p>
223
  <div class="mt-4 flex justify-end">
224
+ <button id="openSaved" class="text-sm blue-button px-4 py-2 text-white rounded-md shadow-sm">
225
+ Voir Sauvegardes
 
 
 
226
  </button>
227
  </div>
228
  </header>
229
 
230
+ <main id="mainContent">
231
+ <form id="problemForm" class="space-y-6" novalidate>
232
+ <!-- Tabs for model selection -->
233
  <div class="tabs">
234
+ <div class="tab active" data-endpoint="solve">Gemini 1.5 Pro</div> <!-- Updated Name -->
235
+ <div class="tab" data-endpoint="solved">Gemini 1.5 Flash</div> <!-- Updated Name -->
236
  </div>
237
 
238
+ <!-- Image upload area -->
239
+ <div class="uploadArea p-8 text-center relative rounded-md" aria-label="Zone de dépôt d'image">
240
  <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">
241
  <div class="space-y-3">
242
+ <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center bg-blue-50">
243
+ <svg class="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
244
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
245
  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" />
246
  </svg>
247
  </div>
248
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
249
+ <p class="text-gray-500 text-sm">ou cliquez pour sélectionner (PNG, JPG, WEBP...)</p>
 
 
 
 
250
  </div>
251
  </div>
252
+ <!-- Image preview -->
253
+ <div id="imagePreview" class="hidden text-center mt-4">
254
+ <img id="previewImage" class="preview-image mx-auto border border-gray-300 rounded" alt="Prévisualisation de l'image">
255
+ </div>
256
+ <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg shadow-md hover:shadow-lg transition duration-200">
257
  Résoudre le problème
258
  </button>
259
  </form>
 
261
  <!-- Loader -->
262
  <div id="loader" class="hidden mt-8 text-center">
263
  <span class="loader"></span>
264
+ <p class="mt-4 text-gray-600 font-medium">Analyse en cours...</p>
265
  </div>
266
 
267
+ <!-- Solution display area -->
268
+ <section id="solution" class="hidden mt-8 space-y-6">
269
+ <!-- Thinking Process Toggle and Content -->
270
+ <div class="border rounded-md overflow-hidden">
271
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-100 hover:bg-gray-200 transition duration-150">
 
272
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
273
+ <div class="flex items-center">
274
+ <span id="timestamp" class="timestamp"></span>
275
+ <!-- Simple chevron icon -->
276
+ <svg id="toggleIcon" class="w-5 h-5 text-gray-600 ml-2 transform transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
277
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
278
+ </svg>
279
+ </div>
280
  </button>
281
+ <div id="thoughtsBox" class="thought-box"> <!-- Content goes inside the box -->
282
+ <div id="thoughtsContent" class="p-4 text-sm text-gray-600"></div>
283
+ </div>
284
  </div>
285
 
286
+ <!-- Answer Content -->
287
+ <div class="border rounded-md">
288
  <div class="flex justify-between items-center p-3 border-b bg-white">
289
+ <h3 class="text-xl font-semibold text-gray-800">Solution Détaillée</h3>
290
+ <button id="saveButton">Sauvegarder</button>
291
+ </div>
292
+ <div id="answerContent" class="p-4 text-gray-700 table-responsive">
293
+ <!-- Content will be rendered here -->
 
 
294
  </div>
 
295
  </div>
 
296
  </section>
297
  </main>
298
  </div>
299
 
300
+ <!-- Saved Solutions Modal -->
301
  <div id="savedModal">
302
+ <div id="savedModalContent">
303
+ <header class="flex justify-between items-center">
304
+ <h2 class="text-2xl font-bold text-gray-800">Solutions Sauvegardées</h2>
305
  <button id="closeSaved" class="text-3xl text-gray-500 hover:text-gray-700">×</button>
306
  </header>
307
+ <div id="savedListContainer">
308
  <ul id="savedList" class="space-y-4">
309
+ <!-- Saved items will be listed here -->
 
310
  </ul>
311
  </div>
312
+ <footer class="text-center">
313
+ <button id="newExercise" class="blue-button w-full py-3 text-white font-medium rounded-lg">
314
+ Résoudre un nouvel exercice
 
 
 
315
  </button>
316
+ </footer>
317
  </div>
318
  </div>
319
 
320
  <script>
321
+ // Ensure Marked and Highlight.js are loaded before configuring
322
+ // Note: hljs might already be available if scripts load synchronously
323
+ // but deferring configuration is safer.
324
+
325
+ function configureMarked() {
326
+ if (typeof marked === 'undefined' || typeof hljs === 'undefined') {
327
+ console.log("Waiting for Marked and Highlight.js...");
328
+ setTimeout(configureMarked, 100); // Check again shortly
329
+ return;
330
+ }
331
+ console.log("Configuring Marked.js");
332
+ marked.setOptions({
333
+ gfm: true, // Enable GitHub Flavored Markdown
334
+ breaks: true, // Use GFM line breaks (single newline = <br>)
335
+ pedantic: false, // Don't be strict about syntax errors
336
+ smartLists: true, // Use smarter list behavior
337
+ smartypants: false, // Don't auto-correct quotes, dashes, etc.
338
+ highlight: function (code, lang) {
339
+ const language = hljs.getLanguage(lang) ? lang : 'plaintext';
340
+ try {
341
+ return hljs.highlight(code, { language: language, ignoreIllegals: true }).value;
342
+ } catch (error) {
343
+ console.error("Highlighting error:", error);
344
+ return hljs.highlight(code, { language: 'plaintext', ignoreIllegals: true }).value; // Fallback
345
+ }
346
+ }
347
+ });
348
+ window.markedConfigured = true; // Flag that configuration is done
349
+ }
350
+
351
+ configureMarked(); // Start configuration attempt
352
+
353
  document.addEventListener('DOMContentLoaded', () => {
354
+ // Element retrieval
355
  const form = document.getElementById('problemForm');
356
  const imageInput = document.getElementById('imageInput');
357
  const loader = document.getElementById('loader');
 
370
  const savedModal = document.getElementById('savedModal');
371
  const savedList = document.getElementById('savedList');
372
  const newExercise = document.getElementById('newExercise');
373
+ const mainContent = document.getElementById('mainContent'); // Not used currently, but kept
374
  const tabs = document.querySelectorAll('.tab');
375
+ const dropZone = document.querySelector('.uploadArea');
376
 
377
+ // State variables
378
  let startTime = null;
379
  let timerInterval = null;
380
+ let thoughtsBuffer = '';
381
+ let answerBuffer = '';
382
+ let currentMode = null; // 'thinking', 'answering'
383
+ let updateTimeout = null;
384
+ let currentEndpoint = 'solve'; // Default endpoint
385
+ let eventSource = null; // To hold the EventSource object for potential closing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
+ // --- Helper Functions ---
 
 
 
 
 
 
 
388
 
389
+ // Update elapsed time
390
  const updateTimestamp = () => {
391
  if (startTime) {
392
  const seconds = Math.floor((Date.now() - startTime) / 1000);
393
+ timestamp.textContent = `(${seconds}s)`;
394
+ } else {
395
+ timestamp.textContent = '';
396
  }
397
  };
398
+ const startTimer = () => { startTime = Date.now(); updateTimestamp(); timerInterval = setInterval(updateTimestamp, 1000); };
399
+ const stopTimer = () => { clearInterval(timerInterval); updateTimestamp(); /* Keep final time */ }; // Keep showing final time
400
 
401
+ // Handle file selection (preview)
402
  const handleFileSelect = file => {
403
  if (!file || !file.type.startsWith('image/')) {
404
+ if(file) { // Only show error if a file was actually selected/dropped
405
+ Swal.fire('Erreur', 'Veuillez sélectionner un fichier image valide.', 'error');
406
+ }
407
+ imagePreview.classList.add('hidden');
408
+ previewImage.src = '';
409
+ imageInput.value = ''; // Clear the input
410
+ return false; // Indicate failure
411
  }
412
  const reader = new FileReader();
413
  reader.onload = e => {
 
415
  imagePreview.classList.remove('hidden');
416
  };
417
  reader.readAsDataURL(file);
418
+ return true; // Indicate success
419
  };
420
 
421
+ // Toggle thoughts box visibility
422
+ const toggleThoughts = () => {
423
+ thoughtsBox.classList.toggle('open');
424
+ toggleIcon.classList.toggle('rotate-180'); // Rotate chevron
425
+ }
426
+ thoughtsToggle.addEventListener('click', toggleThoughts);
427
+
428
+ // Apply syntax highlighting to code blocks
429
+ const applyHighlighting = () => {
430
+ // Check if hljs is available
431
+ if (typeof hljs !== 'undefined') {
432
+ document.querySelectorAll('#answerContent pre code:not(.hljs)').forEach((block) => {
433
+ try {
434
+ hljs.highlightElement(block);
435
+ } catch (error) {
436
+ console.error("Error applying highlight.js:", error, block);
437
+ }
438
+ });
439
+ } else {
440
+ console.warn("highlight.js not ready for highlighting.");
441
+ }
442
+ };
443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
+ // Render MathJax and update display (debounced)
446
+ const typesetMathJax = async () => {
447
+ if (window.mathJaxReady && typeof MathJax !== 'undefined') {
448
+ console.log("Typesetting MathJax...");
449
+ try {
450
+ // Clear previous typesetting state if necessary before re-typesetting
451
+ // MathJax.startup.document.reset(); // Use cautiously, might have side effects
452
+ // MathJax.startup.document.clear(); // Alternative
453
+ await MathJax.typesetPromise([answerContent]); // Target specific container
454
+ console.log("MathJax typesetting complete.");
455
+ } catch (error) {
456
+ console.error('MathJax typesetting error:', error);
457
+ }
458
+ } else {
459
+ console.log("MathJax not ready, deferring typesetting.");
460
+ // No need for setTimeout here, updateDisplay handles debouncing
461
+ }
462
+ };
463
+
464
+
465
+ // Update content display (debounced)
466
+ const updateDisplay = async () => {
467
+ if (!window.markedConfigured) {
468
+ console.log("Marked not configured, deferring display update.");
469
+ scheduleUpdate(); // Reschedule
470
+ return;
471
+ }
472
+ console.log("Updating display...");
473
+ // Use try-catch for parsing as large/complex markdown can sometimes cause issues
474
+ try {
475
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
476
+ answerContent.innerHTML = marked.parse(answerBuffer);
477
+
478
+ // Apply Highlighting *after* setting innerHTML from marked
479
+ applyHighlighting();
480
+
481
+ // Typeset MathJax *after* highlighting (or concurrently if no dependencies)
482
+ await typesetMathJax();
483
+
484
+ // Scroll to bottom (optional)
485
+ answerContent.scrollTop = answerContent.scrollHeight;
486
+ thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
487
+
488
+ } catch (error) {
489
+ console.error("Error parsing Markdown:", error);
490
+ // Display error message to user?
491
+ answerContent.innerHTML = "<p class='text-red-500'>Erreur lors de l'affichage de la réponse.</p>";
492
+ }
493
 
494
+ updateTimeout = null; // Clear timeout flag
495
+ };
496
+
497
+
498
+ // Schedule display update (debounce mechanism)
499
+ const scheduleUpdate = () => {
500
+ if (!updateTimeout) {
501
+ updateTimeout = setTimeout(updateDisplay, 250); // Debounce time (250ms)
502
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  };
504
 
505
+ // --- Event Listeners ---
506
+
507
+ // Tab switching
508
+ tabs.forEach(tab => {
509
+ tab.addEventListener('click', () => {
510
+ if (eventSource) { // Prevent switching during generation
511
+ Swal.fire('Attention', 'Veuillez attendre la fin de la génération actuelle avant de changer de modèle.', 'warning');
512
+ return;
513
+ }
514
+ tabs.forEach(t => t.classList.remove('active'));
515
+ tab.classList.add('active');
516
+ currentEndpoint = tab.dataset.endpoint;
517
+ console.log("Switched to endpoint:", currentEndpoint);
518
+ });
519
+ });
520
+
521
+ // File input change
522
+ imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
523
+
524
+ // Drag and Drop
525
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400', 'bg-blue-100'); });
526
+ dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400', 'bg-blue-100'); });
527
+ dropZone.addEventListener('drop', e => {
528
+ e.preventDefault();
529
+ dropZone.classList.remove('border-blue-400', 'bg-blue-100');
530
+ if (e.dataTransfer.files.length > 0) {
531
+ // Pass the file to the input and the handler
532
+ imageInput.files = e.dataTransfer.files;
533
+ handleFileSelect(e.dataTransfer.files[0]);
534
+ }
535
+ });
536
+
537
+ // Form submission (main logic)
538
  form.addEventListener('submit', async e => {
539
  e.preventDefault();
540
  const file = imageInput.files[0];
541
  if (!file) {
542
+ Swal.fire('Image manquante', 'Veuillez sélectionner ou déposer une image.', 'warning');
543
  return;
544
  }
545
+ // Ensure the preview is updated if a file was selected but not dropped
546
+ if (!handleFileSelect(file)) return; // Re-validate just in case
547
 
548
+ // --- Reset UI ---
549
  startTimer();
550
  loader.classList.remove('hidden');
551
  solutionSection.classList.add('hidden');
 
552
  thoughtsContent.innerHTML = '';
553
+ answerContent.innerHTML = '';
554
  thoughtsBuffer = '';
555
+ answerBuffer = '';
556
  currentMode = null;
557
+ if (!thoughtsBox.classList.contains('open')) { // Open thoughts box automatically
558
+ toggleThoughts();
559
+ }
560
+ if (eventSource) { // Close previous connection if any
561
+ eventSource.close();
562
+ console.log("Previous EventSource closed.");
563
+ }
564
 
565
+ // --- Prepare and Send Request ---
566
  const formData = new FormData();
567
  formData.append('image', file);
568
 
569
+ const url = `/${currentEndpoint}`; // Use the selected endpoint
570
+ console.log(`Sending request to: ${url}`);
571
 
572
+ eventSource = new EventSource(url + `?t=${Date.now()}`, { // Use GET for EventSource traditionally, or adapt backend if POST needed via fetch stream
573
+ method: 'POST', // NOTE: EventSource standard doesn't support POST body, this might require a fetch-based stream reader instead
574
+ body: formData // This body part is NON-STANDARD for EventSource and likely won't work directly
575
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
+ // --- Use Fetch API for Streaming with POST ---
578
+ // Replace the EventSource code above with this Fetch-based approach:
579
+ fetch(url, {
580
+ method: 'POST',
581
+ body: formData
582
+ })
583
+ .then(response => {
584
+ if (!response.ok) {
585
+ // Try to read error message from server if available
586
+ return response.json().then(errData => {
587
+ throw new Error(errData.error || `Erreur serveur: ${response.statusText}`);
588
+ }).catch(() => {
589
+ // Fallback if response is not JSON or reading fails
590
+ throw new Error(`Erreur HTTP ${response.status} - ${response.statusText}`);
591
+ });
592
+ }
593
+ if (!response.body) {
594
+ throw new Error("La réponse du serveur n'a pas de corps de flux.");
595
+ }
596
+ const reader = response.body.getReader();
597
+ const decoder = new TextDecoder();
598
+ let buffer = '';
599
+
600
+ function processStream() {
601
+ reader.read().then(({ done, value }) => {
602
+ if (done) {
603
+ console.log("Stream terminé.");
604
+ stopTimer();
605
+ if (buffer) { // Process any remaining data in the buffer
606
+ try {
607
+ const data = JSON.parse(buffer.slice(5)); // Assuming "data: " prefix
608
+ processData(data);
609
+ } catch (parseError){
610
+ console.warn("Could not parse trailing buffer data:", buffer, parseError);
611
+ }
612
+ }
613
+ // Ensure final update is displayed
614
+ if(updateTimeout) clearTimeout(updateTimeout);
615
+ updateDisplay();
616
+ return;
617
+ }
618
+
619
+ buffer += decoder.decode(value, { stream: true });
620
+ // Process buffer line by line (SSE format: data: {...}\n\n)
621
+ const lines = buffer.split('\n\n');
622
+ buffer = lines.pop() || ''; // Keep the last (potentially incomplete) line
623
+
624
+ lines.forEach(line => {
625
+ if (line.startsWith('data: ')) {
626
+ try {
627
+ const jsonData = line.substring(5); // Remove "data: "
628
+ const data = JSON.parse(jsonData);
629
+ processData(data);
630
+ } catch (parseError) {
631
+ console.error('Erreur de parsing JSON:', parseError, 'Data received:', line);
632
+ }
633
+ } else if (line.trim()) {
634
+ console.warn("Ligne SSE reçue non reconnue:", line);
635
+ }
636
+ });
637
+
638
+ scheduleUpdate(); // Schedule UI update after processing chunk
639
+ processStream(); // Continue reading the stream
640
+
641
+ }).catch(streamError => {
642
+ console.error('Erreur de lecture du flux:', streamError);
643
+ Swal.fire('Erreur de Connexion', `Impossible de lire la réponse: ${streamError.message}`, 'error');
644
+ loader.classList.add('hidden');
645
+ stopTimer();
646
+ });
647
+ }
648
+ processStream(); // Start processing the stream
649
+ })
650
+ .catch(error => {
651
+ console.error('Erreur Fetch:', error);
652
+ Swal.fire('Erreur Réseau', `Impossible de contacter le serveur: ${error.message}`, 'error');
653
+ loader.classList.add('hidden');
654
+ stopTimer();
655
+ });
656
 
 
 
657
 
658
+ // --- Process Received Data ---
659
+ const processData = (data) => {
660
+ // console.log("Received data:", data); // Debug log
661
+ if (data.mode) {
662
+ currentMode = data.mode;
663
+ console.log("Mode changed to:", currentMode);
664
+ if (currentMode === 'answering' && loader.classList.contains('hidden') === false) {
665
+ loader.classList.add('hidden');
666
+ solutionSection.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
667
  }
668
+ }
669
+
670
+ if (data.content) {
671
+ const content = data.content;
672
+ const type = data.type;
673
+
674
+ if (currentMode === 'thinking') {
675
+ // Accumulate all thinking content as simple text (or handle types if needed)
676
+ thoughtsBuffer += content;
677
+ } else if (currentMode === 'answering') {
678
+ // Handle different content types for the main answer
679
+ switch(type) {
680
+ case 'text':
681
+ answerBuffer += content;
682
+ break;
683
+ case 'code':
684
+ // Wrap code in Markdown fences with language hint if possible (defaulting to python)
685
+ answerBuffer += `\n\n\`\`\`python\n${content}\n\`\`\`\n\n`;
686
+ break;
687
+ case 'result':
688
+ // Format result as a Markdown blockquote
689
+ const formattedResult = content.split('\n').map(line => `> ${line}`).join('\n');
690
+ answerBuffer += `\n\n${formattedResult}\n\n`;
691
+ break;
692
+ case 'image':
693
+ // --- IMAGE HANDLING CORRECTION ---
694
+ const mimeType = data.mime_type || 'image/png'; // Get MIME type or default
695
+ // Use Markdown image syntax with the correct data URI
696
+ answerBuffer += `\n\n![Image générée](data:${mimeType};base64,${content})\n\n`;
697
+ // --- END IMAGE HANDLING CORRECTION ---
698
+ break;
699
+ default:
700
+ console.warn("Type de contenu inconnu reçu:", type, content);
701
+ answerBuffer += content; // Append as text by default
702
+ }
703
  }
704
+ }
705
+
706
+ if (data.error) {
707
+ console.error("Erreur reçue du serveur:", data.error);
708
+ // Display error prominently in the answer section
709
+ answerBuffer += `\n\n<p class="text-red-600 font-bold">Erreur du serveur: ${data.error}</p>\n\n`;
710
+ stopTimer(); // Stop timer on error
711
+ // Consider stopping the stream/fetch here if possible
712
+ }
713
+ };
714
+
715
+ }); // End form submit listener
716
+
717
+ // --- Local Storage & Modal Logic ---
718
+
719
+ // Save solution
720
+ saveButton.addEventListener('click', () => {
721
+ Swal.fire({
722
+ title: 'Sauvegarder la solution',
723
+ input: 'text',
724
+ inputLabel: 'Nom de la sauvegarde',
725
+ inputPlaceholder: 'Ex: Exercice Intégrales',
726
+ showCancelButton: true,
727
+ confirmButtonText: 'Sauvegarder',
728
+ cancelButtonText: 'Annuler',
729
+ inputValidator: (value) => {
730
+ if (!value) {
731
+ return 'Veuillez entrer un nom !'
732
+ }
733
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
734
+ if (savedExercises[value]) {
735
+ return 'Ce nom existe déjà. Choisissez-en un autre.'
736
+ }
737
+ }
738
+ }).then((result) => {
739
+ if (result.isConfirmed && result.value) {
740
+ const saveName = result.value.trim();
741
+ const saveData = {
742
+ answer: answerContent.innerHTML, // Save rendered HTML
743
+ thinking: thoughtsContent.innerHTML, // Save rendered HTML
744
+ date: new Date().toLocaleString('fr-FR') // Use French locale for date
745
+ };
746
+ let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
747
+ savedExercises[saveName] = saveData;
748
+ try {
749
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
750
+ Swal.fire({ icon: 'success', title: 'Sauvegardé !', text: `Solution enregistrée sous "${saveName}".`, timer: 2000, showConfirmButton: false });
751
+ } catch (e) {
752
+ console.error("Error saving to localStorage:", e);
753
+ // Handle potential storage limits
754
+ Swal.fire('Erreur', 'Impossible de sauvegarder. Le stockage local est peut-être plein.', 'error');
755
+ }
756
+ }
757
+ });
758
  });
759
 
760
+ // Load saved list into modal
761
  const loadSavedList = () => {
762
+ savedList.innerHTML = ''; // Clear previous list
763
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
764
+ const sortedKeys = Object.keys(savedExercises).sort((a, b) => {
765
+ // Sort by date descending (newest first) if possible
766
+ try {
767
+ return new Date(savedExercises[b].date.split('/').reverse().join('-')) - new Date(savedExercises[a].date.split('/').reverse().join('-'));
768
+ } catch { return 0; } // Fallback sort
769
+ });
770
 
771
+
772
+ if (sortedKeys.length === 0) {
773
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
774
  return;
775
  }
776
 
777
+ sortedKeys.forEach(name => {
778
+ const data = savedExercises[name];
779
  const li = document.createElement('li');
780
+ li.className = 'border-b border-gray-200 pb-3 mb-3'; // Styling for list items
781
+ li.innerHTML = `
782
+ <div class="flex justify-between items-center">
783
+ <button class="text-left text-blue-600 hover:underline focus:outline-none" data-save="${encodeURIComponent(name)}">
784
+ <span class="font-medium">${name}</span>
785
+ <span class="block text-gray-500 text-xs mt-1">(${data.date})</span>
786
+ </button>
787
+ <button class="text-red-500 hover:text-red-700 p-1 rounded focus:outline-none focus:ring-2 focus:ring-red-400" data-delete="${encodeURIComponent(name)}" aria-label="Supprimer ${name}">
788
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
789
+ <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" />
790
+ </svg>
791
+ </button>
792
+ </div>
 
 
 
 
 
 
 
 
 
793
  `;
 
 
 
794
  savedList.appendChild(li);
795
  });
796
  };
797
 
798
+ // Handle clicks within the saved list (load or delete)
799
  savedList.addEventListener('click', (e) => {
800
+ const loadButton = e.target.closest('[data-save]');
801
+ const deleteButton = e.target.closest('[data-delete]');
802
+
803
+ // Load saved solution
804
+ if (loadButton) {
805
+ const saveName = decodeURIComponent(loadButton.dataset.save);
806
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
807
+ const data = savedExercises[saveName];
808
  if (data) {
809
+ form.classList.add('hidden'); // Hide form
810
  loader.classList.add('hidden');
811
+ solutionSection.classList.remove('hidden'); // Show solution section
812
+ thoughtsContent.innerHTML = data.thinking; // Load saved HTML
813
+ answerContent.innerHTML = data.answer; // Load saved HTML
814
+
815
+ // Re-apply MathJax and Highlighting to the loaded content
816
+ // Need slight delay for elements to be in DOM reliably?
817
+ setTimeout(async () => {
818
+ applyHighlighting();
819
+ await typesetMathJax();
820
+ // Ensure thoughts box is open if it has content
821
+ if(data.thinking && !thoughtsBox.classList.contains('open')) {
822
+ toggleThoughts();
823
+ } else if (!data.thinking && thoughtsBox.classList.contains('open')) {
824
+ toggleThoughts(); // Close if no thinking content was saved
825
+ }
826
+
827
+ }, 100); // Small delay
828
+
829
+ savedModal.classList.remove('active'); // Close modal
830
  }
831
+ }
832
+
833
+ // Delete saved solution
834
+ if (deleteButton) {
835
+ const deleteName = decodeURIComponent(deleteButton.dataset.delete);
836
  Swal.fire({
837
  title: 'Êtes-vous sûr ?',
838
+ text: `Supprimer la sauvegarde "${deleteName}" ? Cette action est irréversible.`,
839
  icon: 'warning',
840
  showCancelButton: true,
841
+ confirmButtonColor: '#d33', // Red for delete
842
  cancelButtonColor: '#3085d6',
843
+ confirmButtonText: 'Oui, supprimer !',
844
  cancelButtonText: 'Annuler'
845
  }).then((result) => {
846
  if (result.isConfirmed) {
847
+ let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
848
+ delete savedExercises[deleteName];
849
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
850
+ Swal.fire( 'Supprimé !', `La sauvegarde "${deleteName}" a été supprimée.`, 'success' );
851
+ loadSavedList(); // Refresh list in modal
852
  }
853
  });
854
  }
855
  });
856
 
857
+ // Modal open/close
858
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
859
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
860
+ // Close modal if clicking outside the content area
861
+ savedModal.addEventListener('click', (e) => {
862
+ if (e.target === savedModal) { // Check if the click is on the backdrop itself
863
  savedModal.classList.remove('active');
864
  }
865
+ });
866
 
867
 
868
+ // Reset UI for a new exercise (from modal)
869
+ newExercise.addEventListener('click', () => {
870
+ form.reset(); // Reset form fields
871
+ form.classList.remove('hidden'); // Show form
872
+ solutionSection.classList.add('hidden'); // Hide solution
873
+ loader.classList.add('hidden'); // Hide loader
874
+ imagePreview.classList.add('hidden'); // Hide preview
875
+ previewImage.src = '';
 
876
  thoughtsContent.innerHTML = '';
877
  answerContent.innerHTML = '';
878
  thoughtsBuffer = '';
879
+ answerBuffer = '';
880
+ stopTimer(); // Reset timer display
881
+ if (thoughtsBox.classList.contains('open')) { // Close thoughts box
882
+ toggleThoughts();
883
+ }
884
+ savedModal.classList.remove('active'); // Close modal
885
+ });
 
 
886
 
887
+ // Initial check: If MathJax is already ready, set the flag
888
+ if (typeof MathJax !== 'undefined' && MathJax.startup?.promise) {
889
+ MathJax.startup.promise.then(() => {
890
+ window.mathJaxReady = true;
891
+ console.log("MathJax startup promise resolved on load.");
892
+ });
893
+ }
894
 
895
+ }); // End DOMContentLoaded
896
  </script>
897
  </body>
898
  </html>