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

Delete templates/maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +0 -626
templates/maj.html DELETED
@@ -1,626 +0,0 @@
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>