Docfile commited on
Commit
1fc47d5
·
verified ·
1 Parent(s): 5c3f09a

Create maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +626 -0
templates/maj.html ADDED
@@ -0,0 +1,626 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mariam | Solution Mathématique (Optimisé)</title>
7
+ <!-- Tailwind CSS -->
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
9
+
10
+ <!-- Configuration MathJax Optimisée -->
11
+ <script>
12
+ window.MathJax = {
13
+ tex: {
14
+ inlineMath: [['$', '$']], // Délimiteurs pour les maths en ligne
15
+ displayMath: [['$$', '$$']], // Délimiteurs pour les maths en bloc
16
+ processEscapes: true, // Permet d'utiliser \$ pour un dollar littéral
17
+ // Packages TeX chargés. 'autoload' charge dynamiquement les commandes nécessaires.
18
+ // 'ams' pour les environnements AMSMath (align, gather, etc.).
19
+ // 'mhchem' pour la notation chimique (\ce).
20
+ // 'physics' pour des notations physiques utiles (\qty, \vec, \abs, \dv, etc.).
21
+ // 'boldsymbol' pour \boldsymbol.
22
+ packages: {'[+]': ['autoload', 'ams', 'mhchem', 'physics', 'boldsymbol']}
23
+ },
24
+ chtml: {
25
+ // Options spécifiques au rendu CommonHTML (le plus performant)
26
+ // fontURL: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/output/chtml/fonts/woff-v2' // Optionnel: Spécifier l'URL des polices
27
+ },
28
+ options: {
29
+ enableMenu: false, // Désactive le menu contextuel MathJax
30
+ messageStyle: 'none', // Supprime les messages de statut MathJax
31
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'], // Tags à ignorer
32
+ ignoreHtmlClass: 'tex2jax_ignore', // Classe CSS pour ignorer des éléments
33
+ processHtmlClass: 'tex2jax_process' // Classe CSS pour forcer le traitement
34
+ },
35
+ startup: {
36
+ // Fonction appelée une fois MathJax entièrement chargé et prêt
37
+ pageReady: () => {
38
+ console.log('MathJax est complètement chargé et prêt.');
39
+ window.mathJaxReady = true; // Drapeau global pour indiquer que MathJax est prêt
40
+ }
41
+ }
42
+ };
43
+ </script>
44
+ <!-- Chargement de MathJax (version tex-mml-chtml pour compatibilité MML, tex-chtml peut être légèrement plus rapide) -->
45
+ <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
46
+ <!-- Chargement de Marked.js pour le rendu Markdown -->
47
+ <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
48
+
49
+ <style>
50
+ /* Importation de la police Google Fonts */
51
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
52
+
53
+ body {
54
+ font-family: 'Space Grotesk', sans-serif;
55
+ /* Amélioration du rendu du texte */
56
+ -webkit-font-smoothing: antialiased;
57
+ -moz-osx-font-smoothing: grayscale;
58
+ }
59
+
60
+ /* Styles pour la zone d'upload */
61
+ .uploadArea {
62
+ background: #f3f4f6; /* bg-gray-100 */
63
+ border: 2px dashed #d1d5db; /* border-gray-300 */
64
+ transition: border-color 0.2s ease-in-out;
65
+ }
66
+ .uploadArea:hover {
67
+ border-color: #3b82f6; /* border-blue-500 */
68
+ }
69
+
70
+ /* Styles pour le bouton */
71
+ .blue-button {
72
+ background: #3b82f6; /* bg-blue-500 */
73
+ transition: background-color 0.2s ease-in-out;
74
+ }
75
+ .blue-button:hover {
76
+ background: #2563eb; /* bg-blue-600 */
77
+ }
78
+
79
+ /* Styles pour l'indicateur de chargement */
80
+ .loader {
81
+ width: 48px;
82
+ height: 48px;
83
+ border: 3px solid #3b82f6; /* border-blue-500 */
84
+ border-bottom-color: transparent;
85
+ border-radius: 50%;
86
+ display: inline-block;
87
+ box-sizing: border-box; /* Ajout pour un dimensionnement plus prévisible */
88
+ animation: rotation 1s linear infinite;
89
+ }
90
+ @keyframes rotation {
91
+ 0% { transform: rotate(0deg); }
92
+ 100% { transform: rotate(360deg); }
93
+ }
94
+
95
+ /* Styles pour la boîte de réflexion (accordéon) */
96
+ .thought-box {
97
+ transition: max-height 0.3s ease-out;
98
+ max-height: 0;
99
+ overflow: hidden;
100
+ background-color: #f9fafb; /* bg-gray-50 légère nuance */
101
+ border-radius: 0 0 0.5rem 0.5rem; /* Coins arrondis en bas */
102
+ }
103
+ .thought-box.open {
104
+ /* Hauteur max plus grande pour accommoder plus de contenu si nécessaire */
105
+ max-height: 800px; /* Ajustable */
106
+ border-top: 1px solid #e5e7eb; /* border-gray-200 */
107
+ }
108
+
109
+ /* Styles pour les zones de contenu (réflexion et réponse) */
110
+ #thoughtsContent, #answerContent {
111
+ /* Hauteur max plus grande */
112
+ max-height: 800px; /* Ajustable */
113
+ overflow-y: auto;
114
+ scroll-behavior: smooth;
115
+ white-space: pre-wrap; /* Conserve les retours à la ligne et espaces */
116
+ word-wrap: break-word; /* Coupe les mots longs pour éviter le débordement */
117
+ background-color: #ffffff; /* Fond blanc pour la lisibilité */
118
+ padding: 1rem; /* Espacement interne */
119
+ border-radius: 0.375rem; /* rounded-md */
120
+ border: 1px solid #e5e7eb; /* border-gray-200 */
121
+ }
122
+ #thoughtsContent {
123
+ background-color: #f9fafb; /* bg-gray-50 pour distinguer */
124
+ }
125
+
126
+ /* Style pour l'image de prévisualisation */
127
+ .preview-image {
128
+ max-width: 100%; /* Utilise la largeur disponible */
129
+ height: auto; /* Garde les proportions */
130
+ max-height: 350px; /* Limite la hauteur maximale */
131
+ object-fit: contain; /* Assure que l'image entière est visible */
132
+ margin-top: 1rem; /* Espace au-dessus */
133
+ border: 1px solid #e5e7eb; /* Bordure légère */
134
+ border-radius: 0.375rem; /* Coins arrondis */
135
+ }
136
+
137
+ /* Style pour le timestamp */
138
+ .timestamp {
139
+ color: #3b82f6; /* text-blue-500 */
140
+ font-size: 0.85em; /* Légèrement plus petit */
141
+ margin-left: 12px;
142
+ font-weight: 500;
143
+ }
144
+
145
+ /* Styles pour les tableaux (générés par Marked) */
146
+ table {
147
+ border-collapse: collapse;
148
+ width: 100%;
149
+ margin-bottom: 1rem;
150
+ border: 1px solid #d1d5db; /* border-gray-300 */
151
+ }
152
+ th, td {
153
+ border: 1px solid #d1d5db; /* border-gray-300 */
154
+ padding: 0.75rem; /* p-3 */
155
+ text-align: left;
156
+ }
157
+ th {
158
+ background-color: #f3f4f6; /* bg-gray-100 */
159
+ font-weight: 600; /* font-semibold */
160
+ }
161
+ /* Conteneur pour rendre les tableaux responsive */
162
+ .table-responsive {
163
+ overflow-x: auto;
164
+ /* Style de la barre de défilement (optionnel, amélioration esthétique) */
165
+ scrollbar-width: thin;
166
+ scrollbar-color: #a0aec0 #e2e8f0;
167
+ }
168
+ .table-responsive::-webkit-scrollbar {
169
+ height: 8px;
170
+ }
171
+ .table-responsive::-webkit-scrollbar-track {
172
+ background: #e2e8f0;
173
+ border-radius: 4px;
174
+ }
175
+ .table-responsive::-webkit-scrollbar-thumb {
176
+ background-color: #a0aec0;
177
+ border-radius: 4px;
178
+ border: 2px solid #e2e8f0;
179
+ }
180
+
181
+ /* Avertissement de performance */
182
+ .performance-warning {
183
+ color: #dc2626; /* text-red-600 */
184
+ font-weight: bold;
185
+ font-size: 1.1em; /* Légèrement plus grand */
186
+ margin-top: 1rem;
187
+ margin-bottom: 1.5rem;
188
+ padding: 0.75rem;
189
+ background-color: #fee2e2; /* bg-red-100 */
190
+ border: 1px solid #fca5a5; /* border-red-300 */
191
+ border-radius: 0.375rem; /* rounded-md */
192
+ text-align: center;
193
+ }
194
+
195
+ </style>
196
+ </head>
197
+ <body class="p-4 bg-gray-50 min-h-screen">
198
+ <div class="max-w-4xl mx-auto bg-white p-6 md:p-8 rounded-lg shadow-md">
199
+ <header class="text-center mb-8">
200
+ <h1 class="text-3xl md:text-4xl font-bold text-blue-600">Mariam - M-0</h1>
201
+ <p class="text-gray-600 mt-2">Solution Mathématique/Physique/Chimie Intelligente</p>
202
+ <!-- Avertissement de performance conservé comme demandé -->
203
+ <p class="performance-warning">
204
+ Vous utilisez actuellement les modèles/performances moyens. Accédez à des performances supérieures avec un abonnement premium !
205
+ </p>
206
+ </header>
207
+
208
+ <main>
209
+ <form id="problemForm" class="space-y-6" novalidate>
210
+ <!-- Zone d'upload améliorée -->
211
+ <div class="uploadArea p-6 md:p-8 text-center relative rounded-lg cursor-pointer" aria-label="Zone de dépôt d'image du problème">
212
+ <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image" aria-describedby="upload-instructions">
213
+ <div class="flex flex-col items-center justify-center space-y-3" id="upload-instructions">
214
+ <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center text-blue-500">
215
+ <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
216
+ <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" />
217
+ </svg>
218
+ </div>
219
+ <p class="text-gray-700 font-medium">Déposez l'image du problème ici</p>
220
+ <p class="text-gray-500 text-sm">ou cliquez pour sélectionner un fichier</p>
221
+ <p class="text-xs text-gray-400 mt-1">(Formats supportés: JPG, PNG, WEBP, etc.)</p>
222
+ </div>
223
+ </div>
224
+
225
+ <!-- Zone de prévisualisation de l'image -->
226
+ <div id="imagePreview" class="hidden text-center">
227
+ <p class="text-sm font-medium text-gray-700 mb-2">Image sélectionnée :</p>
228
+ <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image sélectionnée">
229
+ <button type="button" id="removeImageBtn" class="mt-2 text-sm text-red-600 hover:text-red-800" aria-label="Retirer l'image sélectionnée">Retirer l'image</button>
230
+ </div>
231
+
232
+ <!-- Bouton de soumission -->
233
+ <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg text-lg hover:shadow-lg transition-shadow duration-200 ease-in-out">
234
+ Résoudre le problème
235
+ </button>
236
+ </form>
237
+
238
+ <!-- Indicateur de chargement -->
239
+ <div id="loader" class="hidden mt-8 text-center" aria-busy="true" aria-label="Analyse en cours">
240
+ <span class="loader"></span>
241
+ <p class="mt-4 text-gray-600 font-medium">Analyse en cours...</p>
242
+ </div>
243
+
244
+ <!-- Section de la solution -->
245
+ <section id="solution" class="hidden mt-10 space-y-8">
246
+ <!-- Section Réflexion (Accordéon) -->
247
+ <div class="border rounded-lg shadow-sm overflow-hidden">
248
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-100 hover:bg-gray-200 transition-colors duration-150 ease-in-out" aria-expanded="true" aria-controls="thoughtsBox">
249
+ <span class="font-medium text-gray-800 text-lg">Processus de Réflexion</span>
250
+ <div class="flex items-center">
251
+ <span id="timestamp" class="timestamp"></span>
252
+ <svg id="toggleIcon" class="w-5 h-5 text-gray-600 ml-2 transform rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
253
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
254
+ </svg>
255
+ </div>
256
+ </button>
257
+ <div id="thoughtsBox" class="thought-box open">
258
+ {/* aria-live="polite" annonce les changements aux lecteurs d'écran quand le contenu est stable */}
259
+ <div id="thoughtsContent" class="p-4 text-gray-700 text-sm" aria-live="polite"></div>
260
+ </div>
261
+ </div>
262
+
263
+ <!-- Section Solution -->
264
+ <div class="border-t pt-6">
265
+ <div class="flex justify-between items-center mb-4">
266
+ <h3 class="text-xl font-bold text-gray-800">Solution Détaillée</h3>
267
+ {/* Le bouton de téléchargement PDF a été supprimé comme demandé */}
268
+ </div>
269
+ {/* aria-live="polite" annonce les changements aux lecteurs d'écran */}
270
+ <div id="answerContent" class="text-gray-800 table-responsive leading-relaxed" aria-live="polite"></div>
271
+ </div>
272
+ </section>
273
+ </main>
274
+
275
+ <footer class="text-center mt-10 text-sm text-gray-500">
276
+ <p>© 2024 Mariam AI. Tous droits réservés.</p>
277
+ </footer>
278
+ </div>
279
+
280
+ <script>
281
+ // Attend que le DOM soit entièrement chargé
282
+ document.addEventListener('DOMContentLoaded', () => {
283
+ // Références aux éléments du DOM
284
+ const form = document.getElementById('problemForm');
285
+ const imageInput = document.getElementById('imageInput');
286
+ const loader = document.getElementById('loader');
287
+ const solutionSection = document.getElementById('solution');
288
+ const thoughtsContent = document.getElementById('thoughtsContent');
289
+ const answerContent = document.getElementById('answerContent');
290
+ const thoughtsToggle = document.getElementById('thoughtsToggle');
291
+ const thoughtsBox = document.getElementById('thoughtsBox');
292
+ const toggleIcon = document.getElementById('toggleIcon');
293
+ const imagePreview = document.getElementById('imagePreview');
294
+ const previewImage = document.getElementById('previewImage');
295
+ const removeImageBtn = document.getElementById('removeImageBtn');
296
+ const timestamp = document.getElementById('timestamp');
297
+ const uploadArea = document.querySelector('.uploadArea');
298
+ const uploadInstructions = document.getElementById('upload-instructions');
299
+
300
+ // Variables d'état
301
+ let startTime = null;
302
+ let timerInterval = null;
303
+ let thoughtsBuffer = '';
304
+ let answerBuffer = '';
305
+ let currentMode = null; // 'thinking' or 'answering'
306
+ let updateTimeout = null; // Pour le debounce du rendu
307
+ let mathJaxProcessing = false; // Pour éviter les rendus MathJax concurrents
308
+
309
+ // Configuration de Marked.js
310
+ marked.setOptions({
311
+ gfm: true, // Active GitHub Flavored Markdown (tableaux, etc.)
312
+ breaks: true, // Convertit les sauts de ligne simples en <br>
313
+ mangle: false, // Désactive l'obfuscation des emails (si nécessaire)
314
+ headerIds: false, // Désactive la génération d'ID pour les en-têtes
315
+ // Ajout d'un renderer personnalisé si besoin de modifier le HTML généré
316
+ // renderer: new marked.Renderer()
317
+ });
318
+
319
+ // --- Fonctions Utilitaires ---
320
+
321
+ // Met à jour le timestamp affichant le temps écoulé
322
+ const updateTimestamp = () => {
323
+ if (startTime) {
324
+ const seconds = Math.floor((Date.now() - startTime) / 1000);
325
+ timestamp.textContent = `${seconds}s`;
326
+ }
327
+ };
328
+
329
+ // Démarre le chronomètre
330
+ const startTimer = () => {
331
+ startTime = Date.now();
332
+ timestamp.textContent = '0s'; // Affichage initial
333
+ // Met à jour toutes les secondes
334
+ timerInterval = setInterval(updateTimestamp, 1000);
335
+ };
336
+
337
+ // Arrête le chronomètre
338
+ const stopTimer = () => {
339
+ clearInterval(timerInterval);
340
+ timerInterval = null;
341
+ // Met à jour une dernière fois pour le temps final exact
342
+ if (startTime) updateTimestamp();
343
+ startTime = null; // Réinitialise pour la prochaine exécution
344
+ };
345
+
346
+ // Réinitialise l'état de l'interface utilisateur avant une nouvelle requête
347
+ const resetUI = () => {
348
+ loader.classList.add('hidden');
349
+ loader.removeAttribute('aria-busy');
350
+ solutionSection.classList.add('hidden');
351
+ thoughtsContent.innerHTML = '';
352
+ answerContent.innerHTML = '';
353
+ thoughtsBuffer = '';
354
+ answerBuffer = '';
355
+ currentMode = null;
356
+ thoughtsBox.classList.add('open'); // Garder ouvert par défaut pour nouvelle solution
357
+ toggleIcon.classList.add('rotate-180');
358
+ thoughtsToggle.setAttribute('aria-expanded', 'true');
359
+ stopTimer(); // Assure l'arrêt du timer précédent
360
+ timestamp.textContent = ''; // Vide le timestamp
361
+ };
362
+
363
+ // Gère la sélection/prévisualisation d'un fichier image
364
+ const handleFileSelect = file => {
365
+ if (!file || !file.type.startsWith('image/')) {
366
+ alert('Veuillez sélectionner un fichier image valide.');
367
+ clearImagePreview();
368
+ return;
369
+ }
370
+ const reader = new FileReader();
371
+ reader.onload = e => {
372
+ previewImage.src = e.target.result;
373
+ imagePreview.classList.remove('hidden');
374
+ uploadArea.classList.add('hidden'); // Masquer la zone d'upload
375
+ };
376
+ reader.onerror = () => {
377
+ alert('Erreur lors de la lecture du fichier image.');
378
+ clearImagePreview();
379
+ };
380
+ reader.readAsDataURL(file);
381
+ };
382
+
383
+ // Efface la prévisualisation de l'image et réaffiche la zone d'upload
384
+ const clearImagePreview = () => {
385
+ imageInput.value = ''; // Important pour permettre de resélectionner le même fichier
386
+ previewImage.src = '';
387
+ imagePreview.classList.add('hidden');
388
+ uploadArea.classList.remove('hidden'); // Réafficher la zone d'upload
389
+ }
390
+
391
+ // Typeset le contenu de la réponse avec MathJax
392
+ const typesetAnswerIfReady = async () => {
393
+ // Vérifie si MathJax est chargé et qu'aucun rendu n'est déjà en cours
394
+ if (window.mathJaxReady && !mathJaxProcessing) {
395
+ const answerElement = document.getElementById('answerContent');
396
+ if (!answerElement) return; // Sécurité si l'élément disparaît
397
+
398
+ mathJaxProcessing = true; // Marque le début du traitement
399
+ try {
400
+ // Demande à MathJax de traiter uniquement le contenu de 'answerContent'
401
+ await MathJax.typesetPromise([answerElement]);
402
+ // Défile vers le bas après le rendu pour voir le contenu le plus récent
403
+ answerElement.scrollTop = answerElement.scrollHeight;
404
+ } catch (error) {
405
+ console.error("Erreur lors du typesetting MathJax:", error);
406
+ // Gérer l'erreur de rendu si nécessaire
407
+ } finally {
408
+ mathJaxProcessing = false; // Marque la fin du traitement
409
+ }
410
+ } else if (!window.mathJaxReady) {
411
+ // Si MathJax n'est pas prêt, réessaie après un court délai
412
+ console.log("MathJax pas encore prêt, report du typesetting...");
413
+ setTimeout(typesetAnswerIfReady, 200);
414
+ }
415
+ // Si mathJaxProcessing est true, on attend simplement la fin du rendu en cours
416
+ };
417
+
418
+ // Met à jour l'affichage des sections 'réflexion' et 'réponse'
419
+ const updateDisplay = () => {
420
+ // Efface le timeout précédent s'il existe pour éviter les mises à jour inutiles
421
+ if (updateTimeout) {
422
+ clearTimeout(updateTimeout);
423
+ updateTimeout = null;
424
+ }
425
+
426
+ // Met à jour le contenu HTML à partir des buffers (après parsing Markdown)
427
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
428
+ answerContent.innerHTML = marked.parse(answerBuffer);
429
+
430
+ // Demande le rendu MathJax pour la section réponse
431
+ // Le rendu se fera de manière asynchrone
432
+ typesetAnswerIfReady();
433
+
434
+ // Assure que la dernière partie de la réponse est visible (utile pendant le streaming)
435
+ // Fait maintenant dans typesetAnswerIfReady après le rendu pour plus de précision
436
+ // answerContent.scrollTop = answerContent.scrollHeight;
437
+ };
438
+
439
+ // Planifie une mise à jour de l'affichage (debounce)
440
+ const scheduleUpdate = () => {
441
+ // Si une mise à jour est déjà planifiée, ne rien faire
442
+ if (updateTimeout) return;
443
+ // Planifie l'appel à updateDisplay après 150ms
444
+ // Ce délai évite des rendus trop fréquents pendant le streaming rapide
445
+ updateTimeout = setTimeout(updateDisplay, 150);
446
+ };
447
+
448
+
449
+ // --- Gestionnaires d'Événements ---
450
+
451
+ // Bascule l'affichage de la boîte de réflexion (accordéon)
452
+ thoughtsToggle.addEventListener('click', () => {
453
+ const isOpen = thoughtsBox.classList.toggle('open');
454
+ thoughtsToggle.setAttribute('aria-expanded', isOpen);
455
+ toggleIcon.classList.toggle('rotate-180', isOpen); // Pivote l'icône
456
+ });
457
+
458
+ // Gère le changement de fichier dans l'input
459
+ imageInput.addEventListener('change', e => {
460
+ if (e.target.files && e.target.files.length > 0) {
461
+ handleFileSelect(e.target.files[0]);
462
+ } else {
463
+ clearImagePreview(); // Si l'utilisateur annule la sélection
464
+ }
465
+ });
466
+
467
+ // Bouton pour retirer l'image prévisualisée
468
+ removeImageBtn.addEventListener('click', clearImagePreview);
469
+
470
+ // Gestion du Drag & Drop sur la zone d'upload
471
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
472
+ uploadArea.addEventListener(eventName, e => {
473
+ e.preventDefault();
474
+ e.stopPropagation(); // Empêche la propagation aux éléments parents
475
+ }, false);
476
+ });
477
+
478
+ ['dragenter', 'dragover'].forEach(eventName => {
479
+ uploadArea.addEventListener(eventName, () => {
480
+ uploadArea.classList.add('border-blue-500', 'bg-blue-50'); // Feedback visuel
481
+ }, false);
482
+ });
483
+
484
+ ['dragleave', 'drop'].forEach(eventName => {
485
+ uploadArea.addEventListener(eventName, () => {
486
+ uploadArea.classList.remove('border-blue-500', 'bg-blue-50'); // Annule feedback visuel
487
+ }, false);
488
+ });
489
+
490
+ uploadArea.addEventListener('drop', e => {
491
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
492
+ const file = e.dataTransfer.files[0];
493
+ imageInput.files = e.dataTransfer.files; // Met à jour l'input file
494
+ handleFileSelect(file);
495
+ }
496
+ }, false);
497
+
498
+
499
+ // Gère la soumission du formulaire
500
+ form.addEventListener('submit', async e => {
501
+ e.preventDefault(); // Empêche la soumission standard du formulaire
502
+ const file = imageInput.files[0];
503
+
504
+ // Validation simple côté client
505
+ if (!file) {
506
+ alert('Veuillez sélectionner une image contenant le problème.');
507
+ return;
508
+ }
509
+
510
+ resetUI(); // Réinitialise l'interface
511
+ startTimer(); // Démarre le chrono
512
+ loader.classList.remove('hidden'); // Affiche le loader
513
+ loader.setAttribute('aria-busy', 'true');
514
+
515
+ // Prépare les données du formulaire pour l'envoi
516
+ const formData = new FormData();
517
+ formData.append('image', file);
518
+
519
+ try {
520
+ // Envoi de la requête POST au backend avec l'image
521
+ const response = await fetch('/solved', { // Assurez-vous que cette URL est correcte
522
+ method: 'POST',
523
+ body: formData
524
+ // Pas de 'Content-Type': 'multipart/form-data' ici,
525
+ // le navigateur le définit automatiquement avec la bonne boundary pour FormData
526
+ });
527
+
528
+ // Vérifie si la requête a échoué (status hors 2xx)
529
+ if (!response.ok) {
530
+ let errorMsg = `Erreur HTTP ${response.status}: ${response.statusText}`;
531
+ try {
532
+ // Tente de lire un message d'erreur plus détaillé du corps de la réponse
533
+ const errorBody = await response.json();
534
+ errorMsg += `\n${errorBody.error || JSON.stringify(errorBody)}`;
535
+ } catch (parseError) {
536
+ // Si le corps n'est pas du JSON ou est vide
537
+ errorMsg += "\nAucun détail d'erreur supplémentaire fourni par le serveur.";
538
+ }
539
+ throw new Error(errorMsg);
540
+ }
541
+
542
+ // Traitement de la réponse en streaming (Server-Sent Events attendus)
543
+ const reader = response.body?.getReader();
544
+ if (!reader) {
545
+ throw new Error("Impossible d'obtenir le lecteur de flux de la réponse.");
546
+ }
547
+ const decoder = new TextDecoder(); // Pour décoder les bytes en texte (UTF-8 par défaut)
548
+ let buffer = ''; // Buffer pour les données incomplètes
549
+
550
+ // Boucle de lecture du flux
551
+ while (true) {
552
+ const { done, value } = await reader.read(); // Lit un chunk du flux
553
+
554
+ if (done) { // Si le flux est terminé
555
+ // Traite tout reste dans le buffer (si pertinent pour votre format SSE)
556
+ if (buffer.startsWith('data:')) {
557
+ try {
558
+ const data = JSON.parse(buffer.slice(5));
559
+ if (data.content) {
560
+ if (currentMode === 'thinking') thoughtsBuffer += data.content;
561
+ else if (currentMode === 'answering') answerBuffer += data.content;
562
+ }
563
+ } catch (err) { console.error("Erreur parsing final buffer:", err, "Buffer:", buffer); }
564
+ }
565
+ scheduleUpdate(); // Dernière mise à jour de l'affichage
566
+ await updateDisplay(); // Force la dernière mise à jour immédiate après la fin du stream
567
+ break; // Sort de la boucle
568
+ }
569
+
570
+ // Décode le chunk et l'ajoute au buffer
571
+ buffer += decoder.decode(value, { stream: true });
572
+
573
+ // Traite les messages complets dans le buffer (séparés par \n\n)
574
+ const lines = buffer.split('\n\n');
575
+ buffer = lines.pop() ?? ''; // Garde la partie incomplète pour le prochain chunk
576
+
577
+ for (const line of lines) {
578
+ if (!line.startsWith('data:')) continue; // Ignore les lignes non conformes
579
+
580
+ try {
581
+ const jsonData = line.slice(5).trim(); // Extrait le JSON après 'data: '
582
+ if (!jsonData) continue; // Ignore les messages data vides
583
+
584
+ const data = JSON.parse(jsonData);
585
+
586
+ // Changement de mode (thinking -> answering)
587
+ if (data.mode) {
588
+ currentMode = data.mode;
589
+ // Cache le loader et affiche la section solution au premier message reçu
590
+ if (!solutionSection.classList.contains('hidden')) {
591
+ loader.classList.add('hidden');
592
+ loader.removeAttribute('aria-busy');
593
+ solutionSection.classList.remove('hidden');
594
+ }
595
+ }
596
+ // Ajout du contenu au buffer correspondant
597
+ if (data.content) {
598
+ if (currentMode === 'thinking') {
599
+ thoughtsBuffer += data.content;
600
+ } else if (currentMode === 'answering') {
601
+ answerBuffer += data.content;
602
+ }
603
+ // Planifie une mise à jour de l'affichage (debounced)
604
+ scheduleUpdate();
605
+ }
606
+ } catch (error) {
607
+ console.error('Erreur lors du parsing du message SSE JSON:', error, 'Ligne:', line);
608
+ // Peut-être afficher une erreur à l'utilisateur ou ignorer le message erroné
609
+ }
610
+ }
611
+ } // Fin while(true)
612
+
613
+ stopTimer(); // Arrête le chronomètre après la fin du flux
614
+
615
+ } catch (error) {
616
+ console.error('Erreur lors de la résolution du problème:', error);
617
+ // Affiche une alerte plus informative
618
+ alert(`Une erreur est survenue lors de la communication avec le serveur:\n${error.message}`);
619
+ resetUI(); // Réinitialise l'UI en cas d'erreur
620
+ }
621
+ }); // Fin form.addEventListener('submit')
622
+
623
+ }); // Fin document.addEventListener('DOMContentLoaded')
624
+ </script>
625
+ </body>
626
+ </html>