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

Update templates/maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +133 -89
templates/maj.html CHANGED
@@ -6,7 +6,7 @@
6
  <title>Mariam | Solution Mathématique</title>
7
  <!-- Tailwind CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
9
-
10
  <!-- Configuration MathJax -->
11
  <script>
12
  window.MathJax = {
@@ -14,7 +14,9 @@
14
  inlineMath: [['$', '$']],
15
  displayMath: [['$$', '$$']],
16
  processEscapes: true,
17
- packages: {'[+]': ['autoload','ams']}
 
 
18
  },
19
  options: {
20
  enableMenu: false,
@@ -29,7 +31,7 @@
29
  };
30
  </script>
31
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
32
- <!-- Marked.js - Toujours inclus au cas il serait utilisé pour thoughtsContent -->
33
  <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
34
 
35
  <style>
@@ -56,8 +58,6 @@
56
  background: #2563eb;
57
  }
58
 
59
- /* Suppression du style du bouton de téléchargement */
60
-
61
  .loader {
62
  width: 48px;
63
  height: 48px;
@@ -85,8 +85,15 @@
85
  max-height: 500px;
86
  overflow-y: auto;
87
  scroll-behavior: smooth;
88
- white-space: pre-wrap; /* Important pour préserver les espaces/retours ligne du LaTeX brut */
 
89
  }
 
 
 
 
 
 
90
 
91
  .preview-image {
92
  max-width: 300px;
@@ -100,12 +107,12 @@
100
  margin-left: 8px;
101
  }
102
 
103
- /* Styles pour les tables générées par MathJax/Markdown si nécessaire */
104
  table {
105
  border-collapse: collapse;
106
  width: 100%;
107
  margin-bottom: 1rem;
108
- border: 1px solid #d1d5db; /* Ajout bordure table */
109
  }
110
  th, td {
111
  border: 1px solid #d1d5db;
@@ -120,8 +127,6 @@
120
  overflow-x: auto;
121
  }
122
 
123
- /* Suppression du style pour l'impression */
124
-
125
  .performance-warning {
126
  color: red;
127
  font-weight: bold;
@@ -132,19 +137,19 @@
132
  }
133
  </style>
134
  </head>
135
- <body class="p-4">
136
- <div class="max-w-4xl mx-auto">
137
- <header class="p-6 text-center mb-8">
138
  <h1 class="text-4xl font-bold text-blue-600">Mariam - M-0</h1>
139
- <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
140
  <p class="performance-warning">
141
- Vous utilisez actuellement les modèles/performances moyens. Accédez à des performances supérieures avec un abonnement premium !
142
  </p>
143
  </header>
144
 
145
  <main>
146
  <form id="problemForm" class="space-y-6" novalidate>
147
- <div class="uploadArea p-8 text-center relative" aria-label="Zone de dépôt d'image">
148
  <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
149
  <div class="space-y-3">
150
  <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
@@ -157,8 +162,8 @@
157
  </div>
158
  </div>
159
 
160
- <div id="imagePreview" class="hidden text-center">
161
- <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image sélectionnée">
162
  </div>
163
 
164
  <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
@@ -172,21 +177,32 @@
172
  </div>
173
 
174
  <section id="solution" class="hidden mt-8 space-y-6">
175
- <div class="border-t pt-4">
176
- <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
 
177
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
178
  <span id="timestamp" class="timestamp"></span>
179
  </button>
180
- <div id="thoughtsBox" class="thought-box">
181
- <div id="thoughtsContent" class="p-4 text-gray-600"></div>
 
182
  </div>
183
  </div>
184
- <div class="border-t pt-6">
185
- <div class="flex justify-between items-center mb-4">
 
 
186
  <h3 class="text-xl font-bold text-gray-800">Solution</h3>
187
- <!-- Suppression du bouton de téléchargement -->
188
  </div>
189
- <div id="answerContent" class="text-gray-700 table-responsive"></div>
 
 
 
 
 
 
 
 
190
  </div>
191
  </section>
192
  </main>
@@ -212,8 +228,7 @@
212
  let answerBuffer = '';
213
  let currentMode = null;
214
  let updateTimeout = null;
215
-
216
- // Suppression de la fonction generatePDF et de son appel
217
 
218
  const updateTimestamp = () => {
219
  if (startTime) {
@@ -230,9 +245,9 @@
230
 
231
  const stopTimer = () => {
232
  clearInterval(timerInterval);
233
- startTime = null;
 
234
  // Ne pas effacer le timestamp final
235
- // timestamp.textContent = '';
236
  };
237
 
238
  const handleFileSelect = file => {
@@ -254,7 +269,7 @@
254
  const dropZone = document.querySelector('.uploadArea');
255
  dropZone.addEventListener('dragover', e => {
256
  e.preventDefault();
257
- dropZone.classList.add('border-blue-400');
258
  });
259
  dropZone.addEventListener('dragleave', e => {
260
  e.preventDefault();
@@ -263,67 +278,89 @@
263
  dropZone.addEventListener('drop', e => {
264
  e.preventDefault();
265
  dropZone.classList.remove('border-blue-400');
266
- handleFileSelect(e.dataTransfer.files[0]);
 
 
 
267
  });
268
 
269
- // --- Déclaration de typesetContentIfReady ---
270
  const typesetContentIfReady = async () => {
271
- if (window.mathJaxReady && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
272
- // Cible les deux conteneurs si nécessaire, ou juste answerContent si seul lui contient du LaTeX
273
- // Pour être sûr, on peut cibler la section solution entière ou des éléments spécifiques.
274
- // Ici, on cible explicitement answerContent comme dans le code original.
275
- // Si thoughtsContent peut aussi avoir du MathJax, ajoutez-le au tableau.
276
- // MathJax.startup.document.elements = [answerContent, thoughtsContent];
277
- MathJax.startup.document.elements = [answerContent]; // Cible uniquement answerContent
278
  try {
 
 
 
 
279
  await MathJax.typesetPromise();
280
- // Fait défiler vers le bas après le rendu MathJax
281
- answerContent.scrollTop = answerContent.scrollHeight;
 
282
  // Si thoughtsContent est aussi traité:
283
  // thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
284
  } catch (error) {
285
  console.error("Erreur pendant MathJax typesetPromise:", error);
 
 
 
286
  }
 
 
 
287
  } else {
288
- console.log('MathJax pas prêt, report du rendu...');
289
- setTimeout(typesetContentIfReady, 200); // Réessayer
 
290
  }
291
  };
292
 
293
- // --- Déclaration de updateDisplay ---
294
- const updateDisplay = async () => {
295
- // Utilise Marked pour thoughtsContent (supposant qu'il contient du Markdown)
296
- if (typeof marked !== 'undefined') {
297
- thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
 
 
 
 
298
  } else {
299
- thoughtsContent.textContent = thoughtsBuffer; // Fallback si Marked n'est pas chargé
 
300
  }
301
 
302
- // **CORRECTION:** Insère le contenu brut dans answerContent, sans passer par Marked
303
- answerContent.innerHTML = answerBuffer;
304
 
305
  // Déclenche le rendu MathJax sur le contenu mis à jour
306
- await typesetContentIfReady();
307
-
308
- // Scrolling (peut être redondant si fait dans typesetContentIfReady)
309
- thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
310
- // answerContent.scrollTop = answerContent.scrollHeight; // Déplacé dans typesetContentIfReady
311
 
312
- updateTimeout = null;
 
 
313
  };
314
 
315
  const scheduleUpdate = () => {
316
- if (updateTimeout) return;
317
- // Délai légèrement augmenté pour laisser le temps au DOM de se mettre à jour avant MathJax
318
- updateTimeout = setTimeout(updateDisplay, 250);
319
  };
320
 
321
  // Configure Marked (si utilisé pour thoughtsContent)
322
  if (typeof marked !== 'undefined') {
323
  marked.setOptions({
324
- gfm: true,
325
- breaks: true
 
 
326
  });
 
 
327
  }
328
 
329
  form.addEventListener('submit', async e => {
@@ -348,60 +385,67 @@
348
  formData.append('image', file);
349
 
350
  try {
351
- const response = await fetch('/solved', {
352
  method: 'POST',
353
  body: formData
354
  });
355
 
356
  if (!response.ok) {
357
- throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
 
 
358
  }
359
  if (!response.body) {
360
- throw new Error("La réponse ne contient pas de corps.");
361
  }
362
 
363
  const reader = response.body.getReader();
364
  const decoder = new TextDecoder();
365
  let buffer = '';
366
- let firstChunkReceived = false; // Pour afficher la section dès réception
367
 
368
  const processChunk = async ({ done, value }) => {
369
  if (done) {
370
- // Traiter le reste du buffer s'il y en a
371
- if (buffer && buffer.startsWith('data:')) {
372
  try {
373
  const data = JSON.parse(buffer.slice(5));
374
  if (data.content) {
375
- if (currentMode === 'thinking') {
376
- thoughtsBuffer += data.content;
377
- } else if (currentMode === 'answering') {
378
- answerBuffer += data.content;
379
- }
380
  }
381
  } catch(jsonError){
382
  console.error("Erreur JSON dans le buffer final:", jsonError, "Buffer:", buffer);
383
  }
 
 
384
  }
385
- // Assurer une dernière mise à jour après la fin du stream
386
- await updateDisplay();
387
- stopTimer(); // Arrêter le timer à la fin
 
 
388
  console.log("Stream terminé.");
389
  return true; // Indique que le stream est terminé
390
  }
391
-
 
392
  buffer += decoder.decode(value, { stream: true });
 
393
  const lines = buffer.split('\n\n');
394
- buffer = lines.pop() || ''; // Garde la partie incomplète pour le prochain chunk
 
395
 
396
  for (const line of lines) {
397
- if (!line.startsWith('data:')) continue;
398
  try {
399
- const data = JSON.parse(line.slice(5));
400
-
 
 
401
  if (data.mode) {
402
  currentMode = data.mode;
403
- // Afficher la section dès la première donnée reçue après le début du mode
404
- if (!firstChunkReceived) {
405
  loader.classList.add('hidden');
406
  solutionSection.classList.remove('hidden');
407
  firstChunkReceived = true;
@@ -413,12 +457,11 @@
413
  } else if (currentMode === 'answering') {
414
  answerBuffer += data.content;
415
  }
416
- // Mise à jour programmée pour éviter trop d'appels DOM/MathJax
417
  scheduleUpdate();
418
  }
419
  } catch(jsonError) {
420
  console.error("Erreur JSON dans le stream:", jsonError, "Ligne:", line);
421
- // Continuer avec les lignes suivantes si possible
422
  }
423
  }
424
  return false; // Indique que le stream n'est pas terminé
@@ -432,10 +475,11 @@
432
  }
433
 
434
  } catch (error) {
435
- console.error('Erreur lors de la récupération ou du traitement:', error);
436
- alert(`Une erreur est survenue: ${error.message}`);
 
 
437
  loader.classList.add('hidden');
438
- solutionSection.classList.add('hidden'); // Cacher si erreur
439
  stopTimer();
440
  }
441
  });
 
6
  <title>Mariam | Solution Mathématique</title>
7
  <!-- Tailwind CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
9
+
10
  <!-- Configuration MathJax -->
11
  <script>
12
  window.MathJax = {
 
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,
 
31
  };
32
  </script>
33
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
34
+ <!-- Marked.js - Pour le rendu Markdown dans 'thoughtsContent' -->
35
  <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
36
 
37
  <style>
 
58
  background: #2563eb;
59
  }
60
 
 
 
61
  .loader {
62
  width: 48px;
63
  height: 48px;
 
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;
 
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;
 
127
  overflow-x: auto;
128
  }
129
 
 
 
130
  .performance-warning {
131
  color: red;
132
  font-weight: bold;
 
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">
 
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">
 
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 où 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>
 
228
  let answerBuffer = '';
229
  let currentMode = null;
230
  let updateTimeout = null;
231
+ let mathJaxProcessing = false; // Flag pour éviter les rendus concurrents
 
232
 
233
  const updateTimestamp = () => {
234
  if (startTime) {
 
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 => {
 
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();
 
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 où 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 => {
 
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).");
400
  }
401
 
402
  const reader = response.body.getReader();
403
  const decoder = new TextDecoder();
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);
419
  }
420
+ } else if (buffer.trim()) {
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) {
447
  currentMode = data.mode;
448
+ if (!firstChunkReceived) {
 
449
  loader.classList.add('hidden');
450
  solutionSection.classList.remove('hidden');
451
  firstChunkReceived = true;
 
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é
 
475
  }
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
  });