Docfile commited on
Commit
9d3b4d3
·
verified ·
1 Parent(s): dc34e95

Update templates/maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +207 -138
templates/maj.html CHANGED
@@ -14,9 +14,7 @@
14
  inlineMath: [['$', '$']],
15
  displayMath: [['$$', '$$']],
16
  processEscapes: true,
17
- // packages: {'[+]': ['autoload','ams']} // Ancienne config
18
- // CORRECTION: Ajout de 'textmacros' pour mieux supporter \textbf et autres commandes textuelles DANS les maths.
19
- packages: {'[+]': ['autoload','ams', 'textmacros']}
20
  },
21
  options: {
22
  enableMenu: false,
@@ -58,6 +56,7 @@
58
  background: #2563eb;
59
  }
60
 
 
61
  .loader {
62
  width: 48px;
63
  height: 48px;
@@ -72,87 +71,110 @@
72
  100% { transform: rotate(360deg); }
73
  }
74
 
 
75
  .thought-box {
76
  transition: max-height 0.3s ease-out;
77
  max-height: 0;
78
  overflow: hidden;
79
  }
80
  .thought-box.open {
81
- max-height: 500px; /* Ajusté si nécessaire */
82
  }
83
 
 
84
  #thoughtsContent, #answerContent {
85
  max-height: 500px;
86
  overflow-y: auto;
87
  scroll-behavior: smooth;
88
- /* white-space: pre-wrap; */ /* Peut interférer avec le rendu HTML/Markdown/MathJax, désactivé pour être sûr */
89
  word-wrap: break-word; /* Assure le retour à la ligne */
90
  }
91
- /* Style pour le contenu LaTeX brut (avant rendu MathJax), utile pour déboguer */
92
- #answerContent mjx-container {
93
- margin-top: 0.5em;
94
- margin-bottom: 0.5em;
 
95
  }
96
 
97
-
98
  .preview-image {
99
  max-width: 300px;
100
  max-height: 300px;
101
  object-fit: contain;
102
  }
103
 
 
104
  .timestamp {
105
  color: #3b82f6;
106
  font-size: 0.9em;
107
  margin-left: 8px;
108
  }
109
 
110
- /* Styles pour les tables (si générées par Markdown ou MathJax) */
111
  table {
112
  border-collapse: collapse;
113
  width: 100%;
 
114
  margin-bottom: 1rem;
115
  border: 1px solid #d1d5db;
116
  }
117
  th, td {
118
  border: 1px solid #d1d5db;
119
- padding: 0.5rem;
120
  text-align: left;
121
  }
122
  th {
123
  background-color: #f3f4f6;
124
  font-weight: 600;
125
  }
 
126
  .table-responsive {
127
  overflow-x: auto;
 
128
  }
129
 
 
130
  .performance-warning {
131
- color: red;
132
  font-weight: bold;
133
- font-size: 1.2em;
134
  margin-top: 10px;
135
  margin-bottom: 25px;
136
  text-align: center;
 
 
 
 
137
  }
 
 
 
 
 
 
 
 
 
 
 
 
138
  </style>
139
  </head>
140
- <body class="p-4 bg-gray-50"> {/* Ajout d'un fond léger */}
141
- <div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md"> {/* Ajout d'une carte pour le contenu */}
142
- <header class="p-6 text-center mb-8 border-b"> {/* Ajout bordure */}
143
- <h1 class="text-4xl font-bold text-blue-600">Mariam - M-0</h1>
144
- <p class="text-gray-600 mt-2">Solution Mathématique/Physique/Chimie Intelligente</p> {/* Ajout mt-2 */}
145
- <p class="performance-warning">
146
  Vous utilisez actuellement les modèles/performances moyens. Accédez à des performances supérieures avec un abonnement premium !
147
- </p>
148
  </header>
149
 
150
  <main>
151
  <form id="problemForm" class="space-y-6" novalidate>
152
- <div class="uploadArea p-8 text-center relative rounded-md" aria-label="Zone de dépôt d'image"> {/* Ajout rounded-md */}
153
  <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
154
- <div class="space-y-3">
155
- <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
156
  <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
157
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
158
  </svg>
@@ -162,11 +184,11 @@
162
  </div>
163
  </div>
164
 
165
- <div id="imagePreview" class="hidden text-center mt-4"> {/* Ajout mt-4 */}
166
- <img id="previewImage" class="preview-image mx-auto border rounded" alt="Prévisualisation de l'image sélectionnée"> {/* Ajout border rounded */}
167
  </div>
168
 
169
- <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
170
  Résoudre le problème
171
  </button>
172
  </form>
@@ -176,36 +198,47 @@
176
  <p class="mt-4 text-gray-600">Analyse en cours...</p>
177
  </div>
178
 
179
- <section id="solution" class="hidden mt-8 space-y-6">
180
- {/* Section Réflexion */}
181
- <div class="border rounded-md shadow-sm"> {/* Ajout bordure/ombre */}
182
- <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-100 rounded-t-md"> {/* Style bouton */}
183
- <span class="font-medium text-gray-700">Processus de Réflexion</span>
184
- <span id="timestamp" class="timestamp"></span>
 
 
 
 
 
185
  </button>
186
- <div id="thoughtsBox" class="thought-box border-t"> {/* Ajout bordure */}
187
- {/* Utilisation de prose pour améliorer le style du contenu Markdown */}
188
- <div id="thoughtsContent" class="p-4 text-gray-600 prose prose-sm max-w-none"></div>
 
 
189
  </div>
190
  </div>
191
 
192
- {/* Section Solution */}
193
- <div class="border rounded-md shadow-sm"> {/* Ajout bordure/ombre */}
194
- <div class="flex justify-between items-center p-3 bg-gray-100 rounded-t-md border-b"> {/* Style titre */}
195
  <h3 class="text-xl font-bold text-gray-800">Solution</h3>
196
  </div>
197
- {/* table-responsive peut rester sur le conteneur pour le cas une table serait générée */}
198
- <div id="answerContent" class="p-4 text-gray-700 table-responsive">
199
- {/* Le contenu LaTeX sera inséré ici */}
200
- {/* NOTE IMPORTANTE POUR \medskip : */}
201
- {/* MathJax (comme LaTeX) applique \medskip pour l'espace vertical ENTRE les blocs $$...$$ */}
202
- {/* ou via \\[<dimension>] dans des environnements comme align*. */}
203
- {/* Si le backend envoie \medskip A L'INTERIEUR d'un bloc $$...$$, il sera probablement ignoré. */}
204
- {/* La correction doit se faire côté backend pour générer du LaTeX standard. */}
205
  </div>
206
  </div>
207
  </section>
208
  </main>
 
 
 
 
 
209
  </div>
210
 
211
  <script>
@@ -218,6 +251,7 @@
218
  const answerContent = document.getElementById('answerContent');
219
  const thoughtsToggle = document.getElementById('thoughtsToggle');
220
  const thoughtsBox = document.getElementById('thoughtsBox');
 
221
  const imagePreview = document.getElementById('imagePreview');
222
  const previewImage = document.getElementById('previewImage');
223
  const timestamp = document.getElementById('timestamp');
@@ -230,139 +264,165 @@
230
  let updateTimeout = null;
231
  let mathJaxProcessing = false; // Flag pour éviter les rendus concurrents
232
 
 
233
  const updateTimestamp = () => {
234
  if (startTime) {
235
  const seconds = Math.floor((Date.now() - startTime) / 1000);
236
  timestamp.textContent = `${seconds}s`;
 
 
237
  }
238
  };
239
-
240
  const startTimer = () => {
241
  startTime = Date.now();
 
242
  timerInterval = setInterval(updateTimestamp, 1000);
243
- updateTimestamp();
244
  };
245
-
246
  const stopTimer = () => {
247
  clearInterval(timerInterval);
248
- // Ne pas réinitialiser startTime ici pour conserver la durée finale
249
- // startTime = null;
250
- // Ne pas effacer le timestamp final
251
  };
 
 
 
 
 
252
 
 
253
  const handleFileSelect = file => {
254
- if (!file) return;
 
 
 
 
 
255
  const reader = new FileReader();
256
  reader.onload = e => {
257
  previewImage.src = e.target.result;
258
  imagePreview.classList.remove('hidden');
259
  };
 
 
 
 
 
260
  reader.readAsDataURL(file);
261
  };
262
 
 
263
  thoughtsToggle.addEventListener('click', () => {
264
- thoughtsBox.classList.toggle('open');
 
265
  });
266
 
 
267
  imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
268
-
269
  const dropZone = document.querySelector('.uploadArea');
270
  dropZone.addEventListener('dragover', e => {
271
  e.preventDefault();
272
- dropZone.classList.add('border-blue-400'); // Utiliser une classe Tailwind existante
273
  });
274
  dropZone.addEventListener('dragleave', e => {
275
  e.preventDefault();
276
- dropZone.classList.remove('border-blue-400');
277
  });
278
  dropZone.addEventListener('drop', e => {
279
  e.preventDefault();
280
- dropZone.classList.remove('border-blue-400');
281
  if (e.dataTransfer.files.length > 0) {
282
- imageInput.files = e.dataTransfer.files; // Assigne les fichiers déposés à l'input
283
- handleFileSelect(e.dataTransfer.files[0]); // Met à jour l'aperçu
284
  }
285
  });
286
 
287
- // --- Fonction de rendu MathJax ---
288
  const typesetContentIfReady = async () => {
289
- // Vérifie si MathJax est prêt ET qu'un rendu n'est pas déjà en cours
290
  if (window.mathJaxReady && !mathJaxProcessing && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
291
- mathJaxProcessing = true; // Bloque les rendus concurrents
292
  console.log("Début du rendu MathJax...");
293
  try {
294
- // Cible explicitement les conteneurs MathJax doit agir
295
- // Si 'thoughtsContent' peut contenir du LaTeX, ajoutez-le au tableau.
296
- // MathJax.startup.document.elements = [answerContent, thoughtsContent];
297
- MathJax.startup.document.elements = [answerContent]; // Cible answerContent
298
- await MathJax.typesetPromise();
299
  console.log("Rendu MathJax terminé.");
300
- // Fait défiler vers le bas APRÈS le rendu MathJax
301
- answerContent.scrollTop = answerContent.scrollHeight;
302
- // Si thoughtsContent est aussi traité:
303
- // thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
 
 
 
304
  } catch (error) {
305
  console.error("Erreur pendant MathJax typesetPromise:", error);
306
  } finally {
307
- mathJaxProcessing = false; // Libère le verrou
308
  console.log("Flag MathJax débloqué.");
 
 
 
 
 
 
309
  }
310
  } else if (mathJaxProcessing) {
311
- console.log('Rendu MathJax déjà en cours, report...');
312
- // On pourrait replanifier ici si nécessaire, mais le scheduleUpdate devrait suffire
 
 
 
313
  } else {
314
- console.log('MathJax pas prêt ou indéfini, report du rendu...');
315
- // Réessayer un peu plus tard
316
- setTimeout(scheduleUpdate, 300);
317
  }
318
  };
319
 
320
- // --- Fonction de mise à jour de l'affichage ---
321
  const updateDisplay = () => {
322
- // Utilise Marked pour thoughtsContent (supposant Markdown)
323
- // Vérifie si marked est chargé
324
  if (typeof marked !== 'undefined' && marked.parse) {
325
- // Utilise { async: false } pour éviter les problèmes potentiels avec le rendu asynchrone de marked
326
- // avant que MathJax ne s'exécute, bien que ce soit généralement pour les highlighters.
327
- // Plus sûr de garder le flux synchrone ici.
328
- thoughtsContent.innerHTML = marked.parse(thoughtsBuffer || '', { async: false });
 
 
329
  } else {
330
- console.warn("Marked.js n'est pas chargé ou `parse` n'est pas une fonction.");
331
  thoughtsContent.textContent = thoughtsBuffer; // Fallback texte brut
332
  }
333
 
334
- // Insère le contenu brut dans answerContent (MathJax s'en chargera)
335
  answerContent.innerHTML = answerBuffer;
336
 
337
- // Déclenche le rendu MathJax sur le contenu mis à jour
338
- // L'appel à typeset est maintenant géré via scheduleUpdate pour éviter les conflits
339
- // await typesetContentIfReady(); // Déplacé dans scheduleUpdate/appel direct
340
-
341
- // Scrolling géré DANS typesetContentIfReady après le rendu
342
-
343
  updateTimeout = null; // Réinitialise le timeout
344
- // Déclenche le rendu MathJax immédiatement après la mise à jour du DOM
345
- typesetContentIfReady();
346
  };
347
 
348
- const scheduleUpdate = () => {
349
- if (updateTimeout) return; // Si une mise à jour est déjà programmée, ne rien faire
350
- // Délai court pour regrouper les mises à jour rapides du stream
351
- updateTimeout = setTimeout(updateDisplay, 150); // Délai réduit
 
 
 
 
 
352
  };
353
 
354
- // Configure Marked (si utilisé pour thoughtsContent)
355
  if (typeof marked !== 'undefined') {
356
  marked.setOptions({
357
- gfm: true, // Active GitHub Flavored Markdown
358
- breaks: true, // Convertit les sauts de ligne simples en <br>
359
- mangle: false, // Désactive l'obfuscation des emails (souvent inutile ici)
360
- headerIds: false // Désactive la génération d'ID pour les titres (moins de conflits potentiels)
361
  });
362
  } else {
363
  console.warn("Marked.js n'est pas chargé. 'thoughtsContent' sera affiché en texte brut.");
364
  }
365
 
 
366
  form.addEventListener('submit', async e => {
367
  e.preventDefault();
368
  const file = imageInput.files[0];
@@ -371,29 +431,35 @@
371
  return;
372
  }
373
 
374
- startTimer();
 
375
  loader.classList.remove('hidden');
376
  solutionSection.classList.add('hidden');
377
- thoughtsContent.innerHTML = '';
378
- answerContent.innerHTML = '';
379
  thoughtsBuffer = '';
380
  answerBuffer = '';
381
  currentMode = null;
382
- thoughtsBox.classList.add('open'); // Ouvre par défaut
 
 
383
 
384
  const formData = new FormData();
385
  formData.append('image', file);
386
 
387
  try {
388
- const response = await fetch('/solved', { // Assurez-vous que l'URL est correcte
 
389
  method: 'POST',
390
  body: formData
391
  });
392
 
393
  if (!response.ok) {
394
- // Essayer de lire le corps de l'erreur si possible
395
- let errorBody = await response.text();
396
- throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}. ${errorBody ? 'Détails: ' + errorBody : ''}`);
 
 
397
  }
398
  if (!response.body) {
399
  throw new Error("La réponse ne contient pas de corps (ReadableStream).");
@@ -404,15 +470,19 @@
404
  let buffer = '';
405
  let firstChunkReceived = false;
406
 
 
407
  const processChunk = async ({ done, value }) => {
408
  if (done) {
409
- // Traiter le reste du buffer s'il y en a (important !)
410
  if (buffer.startsWith('data:')) {
411
  try {
412
- const data = JSON.parse(buffer.slice(5));
413
- if (data.content) {
414
- if (currentMode === 'thinking') thoughtsBuffer += data.content;
415
- else if (currentMode === 'answering') answerBuffer += data.content;
 
 
 
416
  }
417
  } catch(jsonError){
418
  console.error("Erreur JSON dans le buffer final:", jsonError, "Buffer:", buffer);
@@ -421,26 +491,23 @@
421
  console.warn("Données restantes non traitées dans le buffer final:", buffer);
422
  }
423
 
424
- // Assurer une dernière mise à jour de l'affichage après la fin du stream
425
- if (updateTimeout) clearTimeout(updateTimeout); // Annule le timeout programmé
426
- updateDisplay(); // Force la mise à jour finale immédiate
427
- stopTimer();
428
  console.log("Stream terminé.");
429
- return true; // Indique que le stream est terminé
430
  }
431
 
432
- // Ajoute le nouveau chunk au buffer
433
  buffer += decoder.decode(value, { stream: true });
434
- // Sépare le buffer en lignes basées sur '\n\n' (standard pour SSE)
435
  const lines = buffer.split('\n\n');
436
- // La dernière partie peut être incomplète, on la garde dans le buffer
437
- buffer = lines.pop() || '';
438
 
439
  for (const line of lines) {
440
- if (!line.startsWith('data:')) continue; // Ignore les lignes non valides (commentaires, lignes vides...)
441
  try {
442
- const jsonData = line.slice(5).trim(); // Enlève 'data:' et les espaces
443
- if (!jsonData) continue; // Ignore si vide après 'data:'
444
  const data = JSON.parse(jsonData);
445
 
446
  if (data.mode) {
@@ -457,17 +524,16 @@
457
  } else if (currentMode === 'answering') {
458
  answerBuffer += data.content;
459
  }
460
- // Programme une mise à jour (regroupée)
461
- scheduleUpdate();
462
  }
463
  } catch(jsonError) {
464
  console.error("Erreur JSON dans le stream:", jsonError, "Ligne:", line);
465
  }
466
  }
467
- return false; // Indique que le stream n'est pas terminé
468
  };
469
 
470
- // Boucle de lecture du stream
471
  let streamFinished = false;
472
  while (!streamFinished) {
473
  const { done, value } = await reader.read();
@@ -476,11 +542,14 @@
476
 
477
  } catch (error) {
478
  console.error('Erreur lors de la requête ou du traitement du stream:', error);
479
- // Affiche une erreur plus détaillée à l'utilisateur
480
- answerContent.innerHTML = `<p class="text-red-600 font-bold">Une erreur est survenue :</p><p class="text-red-500 mt-2">${error.message}</p>`;
481
- solutionSection.classList.remove('hidden'); // Affiche la section pour montrer l'erreur
482
  loader.classList.add('hidden');
483
- stopTimer();
 
 
 
 
 
484
  }
485
  });
486
  });
 
14
  inlineMath: [['$', '$']],
15
  displayMath: [['$$', '$$']],
16
  processEscapes: true,
17
+ packages: {'[+]': ['autoload','ams', 'textmacros']} // 'textmacros' ajouté
 
 
18
  },
19
  options: {
20
  enableMenu: false,
 
56
  background: #2563eb;
57
  }
58
 
59
+ /* Style du loader */
60
  .loader {
61
  width: 48px;
62
  height: 48px;
 
71
  100% { transform: rotate(360deg); }
72
  }
73
 
74
+ /* Style de la boîte de réflexion */
75
  .thought-box {
76
  transition: max-height 0.3s ease-out;
77
  max-height: 0;
78
  overflow: hidden;
79
  }
80
  .thought-box.open {
81
+ max-height: 500px; /* Ou une valeur suffisante */
82
  }
83
 
84
+ /* Style des conteneurs de contenu */
85
  #thoughtsContent, #answerContent {
86
  max-height: 500px;
87
  overflow-y: auto;
88
  scroll-behavior: smooth;
 
89
  word-wrap: break-word; /* Assure le retour à la ligne */
90
  }
91
+ /* Espacement pour les équations MathJax */
92
+ #answerContent mjx-container {
93
+ display: block; /* Assure que chaque équation display est sur sa ligne */
94
+ margin-top: 0.8em;
95
+ margin-bottom: 0.8em;
96
  }
97
 
98
+ /* Style de l'aperçu image */
99
  .preview-image {
100
  max-width: 300px;
101
  max-height: 300px;
102
  object-fit: contain;
103
  }
104
 
105
+ /* Style du timestamp */
106
  .timestamp {
107
  color: #3b82f6;
108
  font-size: 0.9em;
109
  margin-left: 8px;
110
  }
111
 
112
+ /* Styles pour les tables */
113
  table {
114
  border-collapse: collapse;
115
  width: 100%;
116
+ margin-top: 1rem; /* Ajout espace avant table */
117
  margin-bottom: 1rem;
118
  border: 1px solid #d1d5db;
119
  }
120
  th, td {
121
  border: 1px solid #d1d5db;
122
+ padding: 0.5rem 0.75rem; /* Ajustement padding */
123
  text-align: left;
124
  }
125
  th {
126
  background-color: #f3f4f6;
127
  font-weight: 600;
128
  }
129
+ /* Conteneur pour tables responsives */
130
  .table-responsive {
131
  overflow-x: auto;
132
+ -webkit-overflow-scrolling: touch; /* Pour un défilement plus fluide sur iOS */
133
  }
134
 
135
+ /* Avertissement performance */
136
  .performance-warning {
137
+ color: #dc2626; /* Rouge plus standard */
138
  font-weight: bold;
139
+ /* font-size: 1.2em; */ /* Taille peut être gérée par Tailwind si appliqué ailleurs */
140
  margin-top: 10px;
141
  margin-bottom: 25px;
142
  text-align: center;
143
+ padding: 0.5rem;
144
+ background-color: #fee2e2; /* Fond rouge léger */
145
+ border: 1px solid #fecaca; /* Bordure rouge léger */
146
+ border-radius: 0.375rem; /* rounded-md */
147
  }
148
+
149
+ /* Styles pour la classe 'prose' si Tailwind Typography n'est pas chargé */
150
+ /* Vous pouvez étendre cela si nécessaire */
151
+ .prose p { margin-bottom: 1em; }
152
+ .prose h1, .prose h2, .prose h3 { margin-bottom: 0.8em; margin-top: 1.5em; font-weight: 600; }
153
+ .prose ul, .prose ol { margin-left: 1.5em; margin-bottom: 1em; }
154
+ .prose li > p { margin-bottom: 0.5em; } /* Espace dans les listes */
155
+ .prose code { background-color: #f3f4f6; padding: 0.2em 0.4em; border-radius: 0.25rem; font-size: 0.9em; }
156
+ .prose pre { background-color: #f3f4f6; padding: 1em; border-radius: 0.375rem; overflow-x: auto; }
157
+ .prose pre code { background-color: transparent; padding: 0; border-radius: 0; }
158
+ .prose blockquote { border-left: 4px solid #d1d5db; padding-left: 1em; margin-left: 0; font-style: italic; color: #4b5563; }
159
+
160
  </style>
161
  </head>
162
+ <body class="p-4 bg-gray-100"> {/* Fond légèrement différent */}
163
+ <div class="max-w-4xl mx-auto bg-white p-6 md:p-8 rounded-lg shadow-lg"> {/* Ombre plus prononcée, padding adaptatif */}
164
+ <header class="pb-6 text-center mb-8 border-b border-gray-200"> {/* Bordure plus légère */}
165
+ <h1 class="text-3xl md:text-4xl font-bold text-blue-600">Mariam - M-0</h1>
166
+ <p class="text-gray-600 mt-2">Solution Mathématique/Physique/Chimie Intelligente</p>
167
+ <div class="performance-warning mt-4 text-sm md:text-base"> {/* Utilisation de la classe stylée */}
168
  Vous utilisez actuellement les modèles/performances moyens. Accédez à des performances supérieures avec un abonnement premium !
169
+ </div>
170
  </header>
171
 
172
  <main>
173
  <form id="problemForm" class="space-y-6" novalidate>
174
+ <div class="uploadArea p-6 md:p-8 text-center relative rounded-md cursor-pointer" aria-label="Zone de dépôt d'image">
175
  <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
176
+ <div class="space-y-3 flex flex-col items-center justify-center"> {/* Centrage vertical */}
177
+ <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center mb-3"> {/* Ajout mb */}
178
  <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
179
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
180
  </svg>
 
184
  </div>
185
  </div>
186
 
187
+ <div id="imagePreview" class="hidden text-center mt-4">
188
+ <img id="previewImage" class="preview-image mx-auto border border-gray-300 rounded" alt="Prévisualisation de l'image sélectionnée">
189
  </div>
190
 
191
+ <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
192
  Résoudre le problème
193
  </button>
194
  </form>
 
198
  <p class="mt-4 text-gray-600">Analyse en cours...</p>
199
  </div>
200
 
201
+ <section id="solution" class="hidden mt-10 space-y-8"> {/* Augmentation espace */}
202
+
203
+ <!-- Section Réflexion -->
204
+ <div class="border border-gray-200 rounded-md shadow-sm overflow-hidden"> {/* Ajout overflow-hidden */}
205
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-100 border-b border-gray-200 hover:bg-gray-200 focus:outline-none">
206
+ <span class="font-medium text-gray-800">Processus de Réflexion</span>
207
+ <span class="flex items-center">
208
+ <span id="timestamp" class="timestamp mr-2"></span>
209
+ <!-- Chevron pour indiquer l'ouverture/fermeture -->
210
+ <svg class="w-4 h-4 text-gray-600 transition-transform duration-300" id="thoughtsChevron" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
211
+ </span>
212
  </button>
213
+ <div id="thoughtsBox" class="thought-box">
214
+ <!-- prose-sm pour taille texte plus petite, max-w-none pour utiliser toute la largeur -->
215
+ <div id="thoughtsContent" class="p-4 text-gray-700 prose prose-sm max-w-none">
216
+ <!-- Contenu Markdown/Texte inséré par JS -->
217
+ </div>
218
  </div>
219
  </div>
220
 
221
+ <!-- Section Solution -->
222
+ <div class="border border-gray-200 rounded-md shadow-sm overflow-hidden">
223
+ <div class="flex justify-between items-center p-3 bg-gray-100 border-b border-gray-200">
224
  <h3 class="text-xl font-bold text-gray-800">Solution</h3>
225
  </div>
226
+ <div id="answerContentWrapper" class="table-responsive"> <!-- Wrapper pour table responsive si nécessaire -->
227
+ <div id="answerContent" class="p-4 text-gray-700">
228
+ <!-- Le contenu LaTeX/HTML sera inséré ici -->
229
+ <!-- Les commentaires problématiques ont été enlevés d'ici -->
230
+ <!-- Rappel (pour le développeur) : La gestion de \medskip doit se faire côté backend -->
231
+ <!-- en générant du LaTeX standard (espace entre $$...$$ ou \\[dim] dans align etc.) -->
232
+ </div>
 
233
  </div>
234
  </div>
235
  </section>
236
  </main>
237
+
238
+ <footer class="mt-10 text-center text-gray-500 text-sm pt-6 border-t border-gray-200">
239
+ Mariam M-0 - Interface de test
240
+ </footer>
241
+
242
  </div>
243
 
244
  <script>
 
251
  const answerContent = document.getElementById('answerContent');
252
  const thoughtsToggle = document.getElementById('thoughtsToggle');
253
  const thoughtsBox = document.getElementById('thoughtsBox');
254
+ const thoughtsChevron = document.getElementById('thoughtsChevron'); // Pour l'icône
255
  const imagePreview = document.getElementById('imagePreview');
256
  const previewImage = document.getElementById('previewImage');
257
  const timestamp = document.getElementById('timestamp');
 
264
  let updateTimeout = null;
265
  let mathJaxProcessing = false; // Flag pour éviter les rendus concurrents
266
 
267
+ // --- Fonctions Timer ---
268
  const updateTimestamp = () => {
269
  if (startTime) {
270
  const seconds = Math.floor((Date.now() - startTime) / 1000);
271
  timestamp.textContent = `${seconds}s`;
272
+ } else {
273
+ timestamp.textContent = ''; // Effacer si pas de timer
274
  }
275
  };
 
276
  const startTimer = () => {
277
  startTime = Date.now();
278
+ if (timerInterval) clearInterval(timerInterval); // Clear existing interval if any
279
  timerInterval = setInterval(updateTimestamp, 1000);
280
+ updateTimestamp(); // Initial display
281
  };
 
282
  const stopTimer = () => {
283
  clearInterval(timerInterval);
284
+ timerInterval = null;
285
+ // Garder l'affichage final du temps
 
286
  };
287
+ const resetTimer = () => {
288
+ stopTimer();
289
+ startTime = null;
290
+ updateTimestamp(); // Clear display
291
+ }
292
 
293
+ // --- Gestion Fichier ---
294
  const handleFileSelect = file => {
295
+ if (!file || !file.type.startsWith('image/')) {
296
+ previewImage.src = '';
297
+ imagePreview.classList.add('hidden');
298
+ if(file) alert("Veuillez sélectionner un fichier image valide.");
299
+ return;
300
+ }
301
  const reader = new FileReader();
302
  reader.onload = e => {
303
  previewImage.src = e.target.result;
304
  imagePreview.classList.remove('hidden');
305
  };
306
+ reader.onerror = () => {
307
+ alert("Erreur lors de la lecture du fichier.");
308
+ previewImage.src = '';
309
+ imagePreview.classList.add('hidden');
310
+ }
311
  reader.readAsDataURL(file);
312
  };
313
 
314
+ // --- Toggle Réflexion ---
315
  thoughtsToggle.addEventListener('click', () => {
316
+ const isOpen = thoughtsBox.classList.toggle('open');
317
+ thoughtsChevron.style.transform = isOpen ? 'rotate(180deg)' : 'rotate(0deg)';
318
  });
319
 
320
+ // --- Input Image & Drag/Drop ---
321
  imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
 
322
  const dropZone = document.querySelector('.uploadArea');
323
  dropZone.addEventListener('dragover', e => {
324
  e.preventDefault();
325
+ dropZone.classList.add('border-blue-400', 'bg-blue-50'); // Style plus visible
326
  });
327
  dropZone.addEventListener('dragleave', e => {
328
  e.preventDefault();
329
+ dropZone.classList.remove('border-blue-400', 'bg-blue-50');
330
  });
331
  dropZone.addEventListener('drop', e => {
332
  e.preventDefault();
333
+ dropZone.classList.remove('border-blue-400', 'bg-blue-50');
334
  if (e.dataTransfer.files.length > 0) {
335
+ imageInput.files = e.dataTransfer.files;
336
+ handleFileSelect(e.dataTransfer.files[0]);
337
  }
338
  });
339
 
340
+ // --- Rendu MathJax ---
341
  const typesetContentIfReady = async () => {
 
342
  if (window.mathJaxReady && !mathJaxProcessing && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
343
+ mathJaxProcessing = true;
344
  console.log("Début du rendu MathJax...");
345
  try {
346
+ // MathJax.startup.document.elements = [answerContent, thoughtsContent]; // Si besoin pour les 2
347
+ MathJax.startup.document.elements = [answerContent];
348
+ await MathJax.typesetPromise([answerContent]); // Cibler explicitement améliore parfois perf
 
 
349
  console.log("Rendu MathJax terminé.");
350
+ // Scroll après rendu
351
+ // Utiliser requestAnimationFrame pour s'assurer que le DOM est stable après MathJax
352
+ requestAnimationFrame(() => {
353
+ answerContent.scrollTop = answerContent.scrollHeight;
354
+ // if (thoughtsContent traité) thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
355
+ });
356
+
357
  } catch (error) {
358
  console.error("Erreur pendant MathJax typesetPromise:", error);
359
  } finally {
360
+ mathJaxProcessing = false;
361
  console.log("Flag MathJax débloqué.");
362
+ // Relancer un check si une mise à jour était en attente pendant le traitement
363
+ if (updateTimeout) {
364
+ clearTimeout(updateTimeout);
365
+ updateTimeout = null;
366
+ scheduleUpdate(0); // Planifier une mise à jour immédiate
367
+ }
368
  }
369
  } else if (mathJaxProcessing) {
370
+ console.log('Rendu MathJax déjà en cours, mise à jour reportée.');
371
+ // La mise à jour sera replanifiée dans le finally
372
+ } else if (!window.mathJaxReady) {
373
+ console.log('MathJax pas prêt, report du rendu...');
374
+ setTimeout(() => scheduleUpdate(100), 250); // Réessayer plus tard
375
  } else {
376
+ console.log('Condition inconnue empêchant le rendu MathJax.');
 
 
377
  }
378
  };
379
 
380
+ // --- Mise à jour Affichage ---
381
  const updateDisplay = () => {
382
+ // Marked pour thoughtsContent
 
383
  if (typeof marked !== 'undefined' && marked.parse) {
384
+ try {
385
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer || '', { async: false });
386
+ } catch (e) {
387
+ console.error("Erreur Marked.parse:", e);
388
+ thoughtsContent.textContent = thoughtsBuffer; // Fallback
389
+ }
390
  } else {
 
391
  thoughtsContent.textContent = thoughtsBuffer; // Fallback texte brut
392
  }
393
 
394
+ // Contenu brut pour answerContent
395
  answerContent.innerHTML = answerBuffer;
396
 
 
 
 
 
 
 
397
  updateTimeout = null; // Réinitialise le timeout
398
+ // Déclenche le rendu MathJax après la mise à jour du DOM
399
+ typesetContentIfReady();
400
  };
401
 
402
+ // --- Planification Mise à jour (Debounce) ---
403
+ const scheduleUpdate = (delay = 100) => { // délai par défaut
404
+ if (mathJaxProcessing) {
405
+ console.log("Mise à jour différée car MathJax est en cours.");
406
+ // La mise à jour sera déclenchée à la fin du rendu MathJax
407
+ return;
408
+ }
409
+ if (updateTimeout) clearTimeout(updateTimeout); // Annule le précédent timeout
410
+ updateTimeout = setTimeout(updateDisplay, delay); // Programme la nouvelle mise à jour
411
  };
412
 
413
+ // --- Configuration Marked ---
414
  if (typeof marked !== 'undefined') {
415
  marked.setOptions({
416
+ gfm: true,
417
+ breaks: true,
418
+ mangle: false,
419
+ headerIds: false
420
  });
421
  } else {
422
  console.warn("Marked.js n'est pas chargé. 'thoughtsContent' sera affiché en texte brut.");
423
  }
424
 
425
+ // --- Soumission Formulaire ---
426
  form.addEventListener('submit', async e => {
427
  e.preventDefault();
428
  const file = imageInput.files[0];
 
431
  return;
432
  }
433
 
434
+ resetTimer(); // Remet le timer à zéro
435
+ startTimer(); // Démarre le timer
436
  loader.classList.remove('hidden');
437
  solutionSection.classList.add('hidden');
438
+ thoughtsContent.innerHTML = ''; // Clear previous content
439
+ answerContent.innerHTML = ''; // Clear previous content
440
  thoughtsBuffer = '';
441
  answerBuffer = '';
442
  currentMode = null;
443
+ if (!thoughtsBox.classList.contains('open')) { // Ouvre si fermé
444
+ thoughtsToggle.click();
445
+ }
446
 
447
  const formData = new FormData();
448
  formData.append('image', file);
449
 
450
  try {
451
+ // Remplacez '/solved' par votre endpoint réel si différent
452
+ const response = await fetch('/solved', {
453
  method: 'POST',
454
  body: formData
455
  });
456
 
457
  if (!response.ok) {
458
+ let errorBody = 'Impossible de lire les détails de l'erreur.';
459
+ try {
460
+ errorBody = await response.text();
461
+ } catch (readError) { /* Ignore si on ne peut pas lire */ }
462
+ throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}. ${errorBody}`);
463
  }
464
  if (!response.body) {
465
  throw new Error("La réponse ne contient pas de corps (ReadableStream).");
 
470
  let buffer = '';
471
  let firstChunkReceived = false;
472
 
473
+ // --- Traitement Stream ---
474
  const processChunk = async ({ done, value }) => {
475
  if (done) {
476
+ // Traitement buffer final
477
  if (buffer.startsWith('data:')) {
478
  try {
479
+ const jsonData = buffer.slice(5).trim();
480
+ if(jsonData) {
481
+ const data = JSON.parse(jsonData);
482
+ if (data.content) {
483
+ if (currentMode === 'thinking') thoughtsBuffer += data.content;
484
+ else if (currentMode === 'answering') answerBuffer += data.content;
485
+ }
486
  }
487
  } catch(jsonError){
488
  console.error("Erreur JSON dans le buffer final:", jsonError, "Buffer:", buffer);
 
491
  console.warn("Données restantes non traitées dans le buffer final:", buffer);
492
  }
493
 
494
+ // Mise à jour finale
495
+ scheduleUpdate(0); // Force la mise à jour immédiate
496
+ stopTimer();
 
497
  console.log("Stream terminé.");
498
+ return true; // Stream fini
499
  }
500
 
501
+ // Traitement chunk courant
502
  buffer += decoder.decode(value, { stream: true });
 
503
  const lines = buffer.split('\n\n');
504
+ buffer = lines.pop() || ''; // Garde le reste pour le prochain chunk
 
505
 
506
  for (const line of lines) {
507
+ if (!line.startsWith('data:')) continue;
508
  try {
509
+ const jsonData = line.slice(5).trim();
510
+ if (!jsonData) continue;
511
  const data = JSON.parse(jsonData);
512
 
513
  if (data.mode) {
 
524
  } else if (currentMode === 'answering') {
525
  answerBuffer += data.content;
526
  }
527
+ scheduleUpdate(); // Planifie une mise à jour (regroupée)
 
528
  }
529
  } catch(jsonError) {
530
  console.error("Erreur JSON dans le stream:", jsonError, "Ligne:", line);
531
  }
532
  }
533
+ return false; // Stream continue
534
  };
535
 
536
+ // Boucle lecture stream
537
  let streamFinished = false;
538
  while (!streamFinished) {
539
  const { done, value } = await reader.read();
 
542
 
543
  } catch (error) {
544
  console.error('Erreur lors de la requête ou du traitement du stream:', error);
545
+ stopTimer(); // Arrête le timer en cas d'erreur aussi
 
 
546
  loader.classList.add('hidden');
547
+ // Affiche l'erreur dans la section solution
548
+ answerContent.innerHTML = `<div class="p-4 bg-red-100 border border-red-300 text-red-700 rounded-md">
549
+ <p class="font-bold">Une erreur est survenue :</p>
550
+ <p class="mt-2 text-sm">${error.message}</p>
551
+ </div>`;
552
+ solutionSection.classList.remove('hidden'); // Affiche la section pour voir l'erreur
553
  }
554
  });
555
  });