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

Update templates/maj.html

Browse files
Files changed (1) hide show
  1. templates/maj.html +123 -332
templates/maj.html CHANGED
@@ -1,20 +1,17 @@
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</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 = {
13
  tex: {
14
  inlineMath: [['$', '$']],
15
  displayMath: [['$$', '$$']],
16
  processEscapes: true,
17
- packages: {'[+]': ['autoload','ams', 'textmacros']} // 'textmacros' ajouté
18
  },
19
  options: {
20
  enableMenu: false,
@@ -29,152 +26,45 @@
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 - Pour le rendu Markdown dans 'thoughtsContent' -->
33
  <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
34
 
35
  <style>
36
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
37
 
38
- body {
39
- font-family: 'Space Grotesk', sans-serif;
40
- }
41
-
42
- .uploadArea {
43
- background: #f3f4f6;
44
- border: 2px dashed #d1d5db;
45
- transition: border-color 0.2s ease;
46
- }
47
- .uploadArea:hover {
48
- border-color: #3b82f6;
49
- }
50
-
51
- .blue-button {
52
- background: #3b82f6;
53
- transition: background-color 0.2s ease;
54
- }
55
- .blue-button:hover {
56
- background: #2563eb;
57
- }
58
-
59
- /* Style du loader */
60
- .loader {
61
- width: 48px;
62
- height: 48px;
63
- border: 3px solid #3b82f6;
64
- border-bottom-color: transparent;
65
- border-radius: 50%;
66
- display: inline-block;
67
- animation: rotation 1s linear infinite;
68
- }
69
- @keyframes rotation {
70
- 0% { transform: rotate(0deg); }
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>
@@ -185,12 +75,10 @@
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>
195
 
196
  <div id="loader" class="hidden mt-8 text-center">
@@ -198,47 +86,25 @@
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,7 +117,6 @@
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');
@@ -262,167 +127,111 @@
262
  let answerBuffer = '';
263
  let currentMode = null;
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,38 +240,31 @@
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).");
466
  }
467
 
468
  const reader = response.body.getReader();
@@ -470,86 +272,75 @@
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);
489
  }
490
  } else if (buffer.trim()) {
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) {
514
- currentMode = data.mode;
515
- if (!firstChunkReceived) {
516
- loader.classList.add('hidden');
517
- solutionSection.classList.remove('hidden');
518
- firstChunkReceived = true;
519
- }
520
  }
521
- if (data.content) {
522
- if (currentMode === 'thinking') {
523
- thoughtsBuffer += data.content;
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();
540
- streamFinished = await processChunk({ done, value });
541
  }
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
  });
 
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</title>
 
7
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
 
 
8
  <script>
9
  window.MathJax = {
10
  tex: {
11
  inlineMath: [['$', '$']],
12
  displayMath: [['$$', '$$']],
13
  processEscapes: true,
14
+ packages: {'[+]': ['autoload','ams', 'textmacros']}
15
  },
16
  options: {
17
  enableMenu: false,
 
26
  };
27
  </script>
28
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
 
29
  <script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
30
 
31
  <style>
32
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
33
 
34
+ body { font-family: 'Space Grotesk', sans-serif; }
35
+ .uploadArea { background: #f3f4f6; border: 2px dashed #d1d5db; transition: border-color 0.2s ease; }
36
+ .uploadArea:hover { border-color: #3b82f6; }
37
+ .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
38
+ .blue-button:hover { background: #2563eb; }
39
+ .loader { width: 48px; height: 48px; border: 3px solid #3b82f6; border-bottom-color: transparent; border-radius: 50%; display: inline-block; animation: rotation 1s linear infinite; }
40
+ @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
41
+ .thought-box { transition: max-height 0.3s ease-out; max-height: 0; overflow: hidden; }
42
+ .thought-box.open { max-height: 500px; }
43
+ #thoughtsContent, #answerContent { max-height: 500px; overflow-y: auto; scroll-behavior: smooth; word-wrap: break-word; }
44
+ #answerContent mjx-container { margin-top: 0.5em; margin-bottom: 0.5em; }
45
+ .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
46
+ .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
47
+ table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; border: 1px solid #d1d5db; }
48
+ th, td { border: 1px solid #d1d5db; padding: 0.5rem; text-align: left; }
49
+ th { background-color: #f3f4f6; font-weight: 600; }
50
+ .table-responsive { overflow-x: auto; }
51
+ .performance-warning { color: red; font-weight: bold; font-size: 1.2em; margin-top: 10px; margin-bottom: 25px; text-align: center; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </style>
53
  </head>
54
+ <body class="p-4 bg-gray-50">
55
+ <div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
56
+ <header class="p-6 text-center mb-8 border-b">
57
+ <h1 class="text-4xl font-bold text-blue-600">Mariam - M-0</h1>
58
  <p class="text-gray-600 mt-2">Solution Mathématique/Physique/Chimie Intelligente</p>
59
+ <p class="performance-warning">Vous utilisez actuellement les modèles/performances moyens. Accédez à des performances supérieures avec un abonnement premium !</p>
 
 
60
  </header>
61
 
62
  <main>
63
  <form id="problemForm" class="space-y-6" novalidate>
64
+ <div class="uploadArea p-8 text-center relative rounded-md" aria-label="Zone de dépôt d'image">
65
  <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
66
+ <div class="space-y-3">
67
+ <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
68
  <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
69
  <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" />
70
  </svg>
 
75
  </div>
76
 
77
  <div id="imagePreview" class="hidden text-center mt-4">
78
+ <img id="previewImage" class="preview-image mx-auto border rounded" alt="Prévisualisation de l'image sélectionnée">
79
  </div>
80
 
81
+ <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">Résoudre le problème</button>
 
 
82
  </form>
83
 
84
  <div id="loader" class="hidden mt-8 text-center">
 
86
  <p class="mt-4 text-gray-600">Analyse en cours...</p>
87
  </div>
88
 
89
+ <section id="solution" class="hidden mt-8 space-y-6">
90
+ <div class="border rounded-md shadow-sm">
91
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-100 rounded-t-md">
92
+ <span class="font-medium text-gray-700">Processus de Réflexion</span>
93
+ <span id="timestamp" class="timestamp"></span>
 
 
 
 
 
 
94
  </button>
95
+ <div id="thoughtsBox" class="thought-box border-t">
96
+ <div id="thoughtsContent" class="p-4 text-gray-600 prose prose-sm max-w-none"></div>
 
 
 
97
  </div>
98
  </div>
99
 
100
+ <div class="border rounded-md shadow-sm">
101
+ <div class="flex justify-between items-center p-3 bg-gray-100 rounded-t-md border-b">
 
102
  <h3 class="text-xl font-bold text-gray-800">Solution</h3>
103
  </div>
104
+ <div id="answerContent" class="p-4 text-gray-700 table-responsive"></div>
 
 
 
 
 
 
 
105
  </div>
106
  </section>
107
  </main>
 
 
 
 
 
108
  </div>
109
 
110
  <script>
 
117
  const answerContent = document.getElementById('answerContent');
118
  const thoughtsToggle = document.getElementById('thoughtsToggle');
119
  const thoughtsBox = document.getElementById('thoughtsBox');
 
120
  const imagePreview = document.getElementById('imagePreview');
121
  const previewImage = document.getElementById('previewImage');
122
  const timestamp = document.getElementById('timestamp');
 
127
  let answerBuffer = '';
128
  let currentMode = null;
129
  let updateTimeout = null;
130
+ let mathJaxProcessing = false;
131
 
 
132
  const updateTimestamp = () => {
133
  if (startTime) {
134
  const seconds = Math.floor((Date.now() - startTime) / 1000);
135
  timestamp.textContent = `${seconds}s`;
 
 
136
  }
137
  };
138
+
139
  const startTimer = () => {
140
  startTime = Date.now();
 
141
  timerInterval = setInterval(updateTimestamp, 1000);
142
+ updateTimestamp();
143
  };
144
+
145
  const stopTimer = () => {
146
  clearInterval(timerInterval);
 
 
147
  };
 
 
 
 
 
148
 
 
149
  const handleFileSelect = file => {
150
+ if (!file) return;
 
 
 
 
 
151
  const reader = new FileReader();
152
  reader.onload = e => {
153
  previewImage.src = e.target.result;
154
  imagePreview.classList.remove('hidden');
155
  };
 
 
 
 
 
156
  reader.readAsDataURL(file);
157
  };
158
 
 
159
  thoughtsToggle.addEventListener('click', () => {
160
+ thoughtsBox.classList.toggle('open');
 
161
  });
162
 
 
163
  imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
164
+
165
  const dropZone = document.querySelector('.uploadArea');
166
  dropZone.addEventListener('dragover', e => {
167
  e.preventDefault();
168
+ dropZone.classList.add('border-blue-400');
169
  });
170
  dropZone.addEventListener('dragleave', e => {
171
  e.preventDefault();
172
+ dropZone.classList.remove('border-blue-400');
173
  });
174
  dropZone.addEventListener('drop', e => {
175
  e.preventDefault();
176
+ dropZone.classList.remove('border-blue-400');
177
  if (e.dataTransfer.files.length > 0) {
178
  imageInput.files = e.dataTransfer.files;
179
  handleFileSelect(e.dataTransfer.files[0]);
180
  }
181
  });
182
 
 
183
  const typesetContentIfReady = async () => {
184
  if (window.mathJaxReady && !mathJaxProcessing && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
185
+ mathJaxProcessing = true;
186
+ console.log("Début du rendu MathJax...");
187
+ try {
188
+ MathJax.startup.document.elements = [answerContent];
189
+ await MathJax.typesetPromise();
190
+ console.log("Rendu MathJax terminé.");
191
+ answerContent.scrollTop = answerContent.scrollHeight;
192
+ } catch (error) {
193
+ console.error("Erreur pendant MathJax typesetPromise:", error);
194
+ } finally {
195
+ mathJaxProcessing = false;
196
+ console.log("Flag MathJax débloqué.");
197
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  } else if (mathJaxProcessing) {
199
+ console.log('Rendu MathJax déjà en cours, report...');
 
 
 
 
200
  } else {
201
+ console.log('MathJax pas prêt ou indéfini, report du rendu...');
202
+ setTimeout(scheduleUpdate, 300);
203
  }
204
  };
205
 
 
206
  const updateDisplay = () => {
 
207
  if (typeof marked !== 'undefined' && marked.parse) {
208
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer || '', { async: false });
 
 
 
 
 
209
  } else {
210
+ console.warn("Marked.js n'est pas chargé ou `parse` n'est pas une fonction.");
211
+ thoughtsContent.textContent = thoughtsBuffer;
212
  }
213
 
 
214
  answerContent.innerHTML = answerBuffer;
215
+ updateTimeout = null;
 
 
216
  typesetContentIfReady();
217
  };
218
 
219
+ const scheduleUpdate = () => {
220
+ if (updateTimeout) return;
221
+ updateTimeout = setTimeout(updateDisplay, 150);
 
 
 
 
 
 
222
  };
223
 
 
224
  if (typeof marked !== 'undefined') {
225
+ marked.setOptions({
226
+ gfm: true,
227
+ breaks: true,
228
+ mangle: false,
229
+ headerIds: false
230
+ });
231
  } else {
232
+ console.warn("Marked.js n'est pas chargé. 'thoughtsContent' sera affiché en texte brut.");
233
  }
234
 
 
235
  form.addEventListener('submit', async e => {
236
  e.preventDefault();
237
  const file = imageInput.files[0];
 
240
  return;
241
  }
242
 
243
+ startTimer();
 
244
  loader.classList.remove('hidden');
245
  solutionSection.classList.add('hidden');
246
+ thoughtsContent.innerHTML = '';
247
+ answerContent.innerHTML = '';
248
  thoughtsBuffer = '';
249
  answerBuffer = '';
250
  currentMode = null;
251
+ thoughtsBox.classList.add('open');
 
 
252
 
253
  const formData = new FormData();
254
  formData.append('image', file);
255
 
256
  try {
 
257
  const response = await fetch('/solved', {
258
  method: 'POST',
259
  body: formData
260
  });
261
 
262
  if (!response.ok) {
263
+ let errorBody = await response.text();
264
+ throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}. ${errorBody ? 'Détails: ' + errorBody : ''}`);
 
 
 
265
  }
266
  if (!response.body) {
267
+ throw new Error("La réponse ne contient pas de corps (ReadableStream).");
268
  }
269
 
270
  const reader = response.body.getReader();
 
272
  let buffer = '';
273
  let firstChunkReceived = false;
274
 
 
275
  const processChunk = async ({ done, value }) => {
276
  if (done) {
 
277
  if (buffer.startsWith('data:')) {
278
  try {
279
+ const data = JSON.parse(buffer.slice(5));
280
+ if (data.content) {
281
+ if (currentMode === 'thinking') thoughtsBuffer += data.content;
282
+ else if (currentMode === 'answering') answerBuffer += data.content;
283
+ }
 
 
 
284
  } catch(jsonError){
285
+ console.error("Erreur JSON dans le buffer final:", jsonError, "Buffer:", buffer);
286
  }
287
  } else if (buffer.trim()) {
288
+ console.warn("Données restantes non traitées dans le buffer final:", buffer);
289
  }
290
 
291
+ if (updateTimeout) clearTimeout(updateTimeout);
292
+ updateDisplay();
293
  stopTimer();
294
  console.log("Stream terminé.");
295
+ return true;
296
  }
297
 
 
298
  buffer += decoder.decode(value, { stream: true });
299
  const lines = buffer.split('\n\n');
300
+ buffer = lines.pop() || '';
301
 
302
  for (const line of lines) {
303
  if (!line.startsWith('data:')) continue;
304
  try {
305
+ const jsonData = line.slice(5).trim();
306
+ if (!jsonData) continue;
307
+ const data = JSON.parse(jsonData);
308
+
309
+ if (data.mode) {
310
+ currentMode = data.mode;
311
+ if (!firstChunkReceived) {
312
+ loader.classList.add('hidden');
313
+ solutionSection.classList.remove('hidden');
314
+ firstChunkReceived = true;
 
315
  }
316
+ }
317
+ if (data.content) {
318
+ if (currentMode === 'thinking') {
319
+ thoughtsBuffer += data.content;
320
+ } else if (currentMode === 'answering') {
321
+ answerBuffer += data.content;
 
322
  }
323
+ scheduleUpdate();
324
+ }
325
  } catch(jsonError) {
326
+ console.error("Erreur JSON dans le stream:", jsonError, "Ligne:", line);
327
  }
328
  }
329
+ return false;
330
  };
331
 
 
332
  let streamFinished = false;
333
  while (!streamFinished) {
334
+ const { done, value } = await reader.read();
335
+ streamFinished = await processChunk({ done, value });
336
  }
337
 
338
  } catch (error) {
339
  console.error('Erreur lors de la requête ou du traitement du stream:', error);
340
+ answerContent.innerHTML = `<p class="text-red-600 font-bold">Une erreur est survenue :</p><p class="text-red-500 mt-2">${error.message}</p>`;
341
+ solutionSection.classList.remove('hidden');
342
  loader.classList.add('hidden');
343
+ stopTimer();
 
 
 
 
 
344
  }
345
  });
346
  });