Docfile commited on
Commit
751980d
·
verified ·
1 Parent(s): 3a063f3

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +491 -532
templates/index.html CHANGED
@@ -3,17 +3,14 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Mariam M-1 | 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
  <!-- SweetAlert2 -->
11
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
12
-
13
  <!-- Highlight.js pour la coloration syntaxique -->
14
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
15
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
16
-
17
  <!-- Configuration de MathJax -->
18
  <script>
19
  window.MathJax = {
@@ -47,89 +44,62 @@
47
  .uploadArea:hover { border-color: #3b82f6; }
48
 
49
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
50
- .blue-button:hover:not(:disabled) { background: #2563eb; } /* Hover only when not disabled */
51
  .blue-button:disabled {
52
- background: #9ca3af; /* Gray background for disabled */
53
  cursor: not-allowed;
54
  }
55
 
 
 
 
56
 
57
  .loader {
58
- width: 48px;
59
- height: 48px;
60
- border: 3px solid #3b82f6;
61
- border-bottom-color: transparent;
62
- border-radius: 50%;
63
- display: inline-block;
64
- animation: rotation 1s linear infinite;
65
  }
66
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
67
 
68
  .thought-box {
69
- transition: max-height 0.3s ease-out;
70
- max-height: 0;
71
- overflow: hidden;
72
  }
73
  .thought-box.open { max-height: 500px; }
74
 
75
  #thoughtsContent, #answerContent {
76
- max-height: 500px;
77
- overflow-y: auto;
78
- scroll-behavior: smooth;
79
- white-space: pre-wrap;
80
  }
81
 
82
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
83
-
84
  .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
85
 
86
  table {
87
- border-collapse: collapse;
88
- width: 100%;
89
- margin-bottom: 1rem;
90
  }
91
  th, td {
92
- border: 1px solid #d1d5db;
93
- padding: 0.5rem;
94
- text-align: left;
95
  }
96
  th { background-color: #f3f4f6; font-weight: 600; }
97
  .table-responsive { overflow-x: auto; }
98
 
99
- /* Style pour le bouton Sauvegarder afin de le mettre en évidence */
100
  #saveButton {
101
- background: #3b82f6;
102
- color: white;
103
- padding: 0.5rem 1rem;
104
- border-radius: 0.375rem;
105
- transition: background-color 0.2s ease;
106
  }
107
  #saveButton:hover { background: #2563eb; }
108
 
109
  /* Modal plein écran pour les sauvegardes */
110
  #savedModal {
111
- display: none;
112
- position: fixed;
113
- inset: 0;
114
- background: rgba(0,0,0,0.5);
115
- z-index: 50;
116
  }
117
  #savedModal.active { display: block; }
118
  #savedModalContent {
119
- background: #fff;
120
- width: 100%;
121
- height: 100%;
122
- overflow-y: auto;
123
  }
124
 
125
- /* Styles spécifiques pour le code et son exécution */
126
  pre {
127
- background-color: #f8f8f8;
128
- border: 1px solid #e2e8f0;
129
- border-radius: 0.375rem;
130
- padding: 1rem;
131
- margin: 1rem 0;
132
- overflow-x: auto;
133
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
134
  }
135
 
@@ -138,34 +108,59 @@
138
  }
139
 
140
  .code-execution-result {
141
- background-color: #f0fff4;
142
- border-left: 4px solid #48bb78;
143
- padding: 1rem;
144
- margin: 1rem 0;
145
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
146
  white-space: pre-wrap;
147
  }
148
 
149
- /* Styles pour les types de contenu spécifiques */
150
  .content-text {}
151
  .content-code { padding: 0; }
152
  .content-result {
153
- background-color: #f0fff4;
154
- border-left: 4px solid #48bb78;
155
- padding: 1rem;
156
- margin: 0.5rem 0;
157
  }
158
  .content-image { margin: 1rem 0; text-align: center; }
159
  .content-image img { max-width: 100%; }
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  </style>
162
  </head>
163
  <body class="p-4">
164
  <div class="max-w-4xl mx-auto">
165
  <header class="p-6 text-center mb-8">
166
- <h1 class="text-4xl font-bold text-blue-600">Mariam M-?</h1>
167
- <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente, avec intégration d'une calculatrice.</p>
168
- <p style="color: red;">Mode standard. Passez au premium pour de très hautes performances.</p>
169
  <div class="mt-4 flex justify-end">
170
  <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
171
  </div>
@@ -187,10 +182,46 @@
187
  <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
188
  </div>
189
  </div>
 
190
  <!-- Aperçu de l'image -->
191
  <div id="imagePreview" class="hidden text-center">
192
  <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
193
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  <!-- Le bouton de soumission -->
195
  <button type="submit" id="submitButton" class="blue-button w-full py-3 text-white font-medium rounded-lg">
196
  Résoudre le problème
@@ -206,7 +237,7 @@
206
  <!-- Zone d'affichage de la solution -->
207
  <section id="solution" class="hidden mt-8 space-y-6 relative">
208
  <div class="border-t pt-4">
209
- <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
210
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
211
  <span id="timestamp" class="timestamp"></span>
212
  </button>
@@ -217,7 +248,6 @@
217
  <div class="border-t pt-6">
218
  <div class="flex justify-between items-center">
219
  <h3 class="text-xl font-bold text-gray-800 mb-4">Solution</h3>
220
- <!-- Bouton Sauvegarder mis en évidence -->
221
  <button id="saveButton">Sauvegarder</button>
222
  </div>
223
  <div id="answerContent" class="text-gray-700 table-responsive"></div>
@@ -231,7 +261,7 @@
231
  <div id="savedModalContent" class="p-6">
232
  <header class="flex justify-between items-center border-b pb-4">
233
  <h2 class="text-2xl font-bold">Sauvegardes</h2>
234
- <button id="closeSaved" class="text-3xl text-gray-600">×</button>
235
  </header>
236
  <div id="savedListContainer" class="mt-4">
237
  <ul id="savedList" class="space-y-4">
@@ -249,580 +279,509 @@
249
  <script>
250
  document.addEventListener('DOMContentLoaded', () => {
251
  // Récupération des éléments
252
- const form = document.getElementById('problemForm');
253
- const imageInput = document.getElementById('imageInput');
254
- const submitButton = document.getElementById('submitButton'); // Get the submit button
255
- const loader = document.getElementById('loader');
256
- const solutionSection = document.getElementById('solution');
257
- const thoughtsContent = document.getElementById('thoughtsContent');
258
- const answerContent = document.getElementById('answerContent');
259
- const thoughtsToggle = document.getElementById('thoughtsToggle');
260
- const thoughtsBox = document.getElementById('thoughtsBox');
261
- const imagePreview = document.getElementById('imagePreview');
262
- const previewImage = document.getElementById('previewImage');
263
- const timestamp = document.getElementById('timestamp');
264
- const saveButton = document.getElementById('saveButton');
265
- const openSaved = document.getElementById('openSaved');
266
- const closeSaved = document.getElementById('closeSaved');
267
- const savedModal = document.getElementById('savedModal');
268
- const savedList = document.getElementById('savedList');
269
- const newExercise = document.getElementById('newExercise');
270
- const mainContent = document.getElementById('mainContent'); // This variable is not used in the original logic, keeping it but it could be removed.
271
-
272
- let startTime = null;
273
- let timerInterval = null;
274
- let thoughtsBuffer = '';
275
- let answerBuffer = '';
276
- let currentMode = null;
277
- let updateTimeout = null;
278
-
279
- // --- Délai de soumission local ---
280
- const COOLDOWN_DURATION = 3 * 60 * 1000; // 3 minutes en millisecondes
281
- const LAST_SUBMISSION_KEY = 'lastSubmissionTime';
282
- let cooldownTimer = null; // Pour l'intervalle de mise à jour du bouton
283
-
284
- // Fonction pour récupérer le timestamp de la dernière soumission depuis localStorage
285
- const getLastSubmissionTime = () => {
286
- const timestamp = localStorage.getItem(LAST_SUBMISSION_KEY);
287
- return timestamp ? parseInt(timestamp, 10) : null;
288
  };
289
 
290
- // Fonction pour enregistrer le timestamp de la soumission actuelle dans localStorage
291
- const setLastSubmissionTime = () => {
292
- localStorage.setItem(LAST_SUBMISSION_KEY, Date.now().toString());
 
 
 
 
 
 
 
293
  };
294
 
295
- // Fonction pour vérifier le délai et mettre à jour l'état du bouton
296
- const updateSubmitButtonState = () => {
297
- const lastSubmit = getLastSubmissionTime();
298
- const now = Date.now();
299
-
300
- if (lastSubmit && now - lastSubmit < COOLDOWN_DURATION) {
301
- const remainingTime = COOLDOWN_DURATION - (now - lastSubmit);
302
- const minutes = Math.floor(remainingTime / 60000);
303
- const seconds = Math.floor((remainingTime % 60000) / 1000);
304
- submitButton.disabled = true;
305
- submitButton.textContent = `Prochaine soumission dans ${minutes}:${seconds.toString().padStart(2, '0')}`;
306
 
307
- // Démarrer le timer si ce n'est pas déjà fait
308
- if (!cooldownTimer) {
309
- cooldownTimer = setInterval(updateSubmitButtonState, 1000);
 
 
 
 
 
 
 
 
310
  }
311
- } else {
312
- // Le délai est terminé ou il n'y a pas de soumission précédente
313
- submitButton.disabled = false;
314
- submitButton.textContent = 'Résoudre le problème';
315
- // Arrêter le timer s'il était en cours
316
- if (cooldownTimer) {
317
- clearInterval(cooldownTimer);
318
- cooldownTimer = null;
319
  }
320
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  };
322
 
323
- // Appeler la fonction au chargement de la page pour initialiser l'état du bouton
324
- updateSubmitButtonState();
325
- // --- Fin Délai de soumission local ---
 
 
326
 
 
 
 
327
 
328
- // Mise à jour du temps écoulé (pour l'analyse en cours)
329
- const updateTimestamp = () => {
330
- if (startTime) {
331
- const seconds = Math.floor((Date.now() - startTime) / 1000);
332
- timestamp.textContent = `${seconds}s`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
  };
335
- const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
336
- const stopTimer = () => { clearInterval(timerInterval); startTime = null; timestamp.textContent = ''; };
337
 
338
- // Affichage de l'image sélectionnée
339
- const handleFileSelect = file => {
340
- if (!file) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  const reader = new FileReader();
342
  reader.onload = e => {
343
- previewImage.src = e.target.result;
344
- imagePreview.classList.remove('hidden');
345
  };
346
  reader.readAsDataURL(file);
347
  };
348
 
349
- thoughtsToggle.addEventListener('click', () => { thoughtsBox.classList.toggle('open'); });
350
- imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
351
-
352
- // Gestion du glisser-déposer
353
- const dropZone = document.querySelector('.uploadArea');
354
- dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400'); });
355
- dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); });
356
- dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); handleFileSelect(e.dataTransfer.files[0]); });
 
 
 
 
 
 
357
 
358
- // Fonction pour appliquer la coloration syntaxique
359
  const applyHighlighting = () => {
360
  document.querySelectorAll('pre code').forEach((block) => {
361
  hljs.highlightBlock(block);
362
  });
363
  };
364
 
365
- // Rendu MathJax et mise à jour de l'affichage
366
- const typesetAnswerIfReady = async () => {
367
- if (window.mathJaxReady) {
368
- // Target specific elements for MathJax typesetting if needed, or document.body
369
- // Targeting answerContent and thoughtsContent is safer for performance
370
- MathJax.startup.document.elements = [document.getElementById('answerContent'), document.getElementById('thoughtsContent')];
371
- await MathJax.typesetPromise();
372
- applyHighlighting(); // Apply highlighting after MathJax
373
- // Keep scrolling behavior only for the main answer content maybe
374
- // answerContent.scrollTop = answerContent.scrollHeight; // Auto-scroll might be jumpy with streaming
375
- } else { setTimeout(typesetAnswerIfReady, 200); }
 
 
376
  };
377
 
378
- const updateDisplay = async () => {
379
- // Use innerHTML = marked.parse(...) for streaming updates
380
- thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
381
- answerContent.innerHTML = marked.parse(answerBuffer);
382
-
383
- // Trigger MathJax typesetting and highlighting *after* updating innerHTML
384
- await typesetAnswerIfReady();
385
-
386
- // Optional: auto-scroll only the answer content if it's the active stream
387
- if (currentMode === 'answering') {
388
- answerContent.scrollTop = answerContent.scrollHeight;
389
- } else if (currentMode === 'thinking') {
390
- thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
391
- }
392
-
393
- updateTimeout = null;
 
 
 
 
 
394
  };
395
 
396
- const scheduleUpdate = () => { if (!updateTimeout) updateTimeout = setTimeout(updateDisplay, 200); };
 
 
397
 
398
- marked.setOptions({
399
- gfm: true,
400
- breaks: true,
401
- highlight: function(code, lang) {
402
- if (lang && hljs.getLanguage(lang)) {
403
- try {
404
- return hljs.highlight(code, { language: lang }).value;
405
- } catch (error) {
406
- console.error("Highlighting error:", error);
407
- return code; // Return original code on error
408
- }
409
- }
410
- // Default highlighting for unrecognised languages or no language specified
411
- try {
412
- return hljs.highlightAuto(code).value;
413
- } catch (error) {
414
- console.error("Auto highlighting error:", error);
415
- return code; // Return original code on error
416
- }
417
  }
418
  });
419
 
420
-
421
- // Envoi de l'image pour résolution
422
- form.addEventListener('submit', async e => {
423
- e.preventDefault(); // Empêcher la soumission par défaut pour gérer le délai
424
-
425
- // --- Vérification du délai ---
426
- const lastSubmit = getLastSubmissionTime();
427
- const now = Date.now();
428
-
429
- if (lastSubmit && now - lastSubmit < COOLDOWN_DURATION) {
430
- const remainingTime = COOLDOWN_DURATION - (now - lastSubmit);
431
- const minutes = Math.floor(remainingTime / 60000);
432
- const seconds = Math.floor((remainingTime % 60000) / 1000);
433
- Swal.fire({
434
- icon: 'warning',
435
- title: 'Veuillez patienter',
436
- text: `Vous devez attendre ${minutes} minute(s) et ${seconds.toString().padStart(2, '0')} seconde(s) avant de soumettre à nouveau.`
437
- });
438
- // Mettre à jour l'état du bouton immédiatement après le message d'erreur
439
- updateSubmitButtonState();
440
- return; // Arrêter le processus de soumission
441
  }
442
- // --- Fin Vérification du délai ---
443
 
444
-
445
- const file = imageInput.files[0];
446
- if (!file) {
447
- Swal.fire({
448
- icon: 'error',
449
- title: 'Image manquante',
450
- text: 'Veuillez sélectionner une image.'
451
- });
452
- // Re-vérifier et potentiellement réactiver le bouton si aucune image n'est sélectionnée
453
- updateSubmitButtonState();
454
  return;
455
  }
456
 
457
- // --- Le délai est passé ou n'existait pas, on procède ---
458
- setLastSubmissionTime(); // Enregistrer le nouveau timestamp *avant* de commencer
459
- updateSubmitButtonState(); // Désactiver et mettre à jour le bouton
460
-
461
 
462
- startTimer();
463
- loader.classList.remove('hidden');
464
- solutionSection.classList.add('hidden');
465
- thoughtsContent.innerHTML = ''; // Vider le contenu précédent
466
- answerContent.innerHTML = ''; // Vider le contenu précédent
467
- thoughtsBuffer = ''; // Vider les buffers
468
- answerBuffer = '';
469
- currentMode = null; // Reset mode
470
-
471
- // thoughtsBox starts open by default, keep it open during processing
472
- thoughtsBox.classList.add('open');
473
 
474
  const formData = new FormData();
475
- formData.append('image', file);
 
476
 
477
  try {
478
- // Hardcode the endpoint to /solved
479
- const response = await fetch('/solved', { method: 'POST', body: formData });
480
-
481
  if (!response.ok) {
482
- // Handle HTTP errors
483
- const errorText = await response.text();
484
- throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
485
  }
486
-
487
- if (!response.body) {
488
- throw new Error('Response body is not available (e.g., not a streaming response)');
489
- }
490
-
491
  const reader = response.body.getReader();
492
  const decoder = new TextDecoder();
493
- let buffer = '';
494
 
495
- const processChunk = async chunk => {
496
- buffer += decoder.decode(chunk, { stream: true });
497
- const lines = buffer.split('\n\n');
498
- buffer = lines.pop(); // Keep the last potentially incomplete line in buffer
499
-
500
- for (const line of lines) {
501
- if (!line.startsWith('data:')) {
502
- // console.warn('Skipping non-data line:', line); // Log non-data lines
503
- continue;
 
 
 
 
504
  }
505
- try {
506
- const data = JSON.parse(line.slice(5));
 
507
 
508
- if (data.mode) {
509
- currentMode = data.mode;
510
- // Hide loader and show solution section once streaming starts
511
- loader.classList.add('hidden');
512
- solutionSection.classList.remove('hidden');
513
- }
514
 
515
- // Process content based on currentMode and data type
516
- if (data.content !== undefined) { // Check for undefined to allow empty strings
517
- if (currentMode === 'thinking') {
518
- thoughtsBuffer += data.content;
519
- } else if (currentMode === 'answering') {
520
- // Handle different types within answering mode
521
- switch(data.type) {
522
- case 'code':
523
- // Use markdown code block syntax
524
- answerBuffer += "\n```\n" + data.content + "\n```\n";
525
- break;
526
- case 'result':
527
- // Format results clearly, handling multiple lines
528
- const formattedResult = data.content.split('\n').map(line => `> ${line}`).join('\n');
529
- answerBuffer += "\n" + formattedResult + "\n";
530
- break;
531
- case 'image':
532
- // Include images as markdown images
533
- answerBuffer += `\n![Résultat](data:image/png;base64,${data.content})\n`;
534
- break;
535
- case 'text': // Explicitly handle text within answering mode
536
- default: // Default behavior is to append text
537
- answerBuffer += data.content;
538
- break;
539
- }
540
- }
541
  }
542
-
543
- if (data.error) {
544
- // Append error to the relevant buffer or main answer buffer
545
- if (currentMode === 'thinking') {
546
- thoughtsBuffer += `\n**Erreur pendant la réflexion:** ${data.error}\n`;
547
- } else { // Assume error relates to the answer process
548
- answerBuffer += `\n**Erreur:** ${data.error}\n`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  }
550
  }
 
551
 
552
- } catch (e) {
553
- console.error('Error parsing JSON data:', line.slice(5), e);
554
- // Append a parsing error message to the output
555
- if (currentMode === 'thinking') { thoughtsBuffer += `\n[Erreur de traitement du flux]`; }
556
- else { answerBuffer += `\n[Erreur de traitement du flux]`; }
 
557
  }
558
- }
559
- scheduleUpdate(); // Schedule display update after processing a batch of lines
560
- };
561
 
562
- // Start processing the stream
563
- while (true) {
564
- const { done, value } = await reader.read();
565
- if (done) {
566
- // Process any remaining buffer
567
- if (buffer.length > 0) { // Process any data left in the buffer
568
- // Even if it's not a full line, decode and append what's left
569
- // Note: This might result in incomplete markdown being displayed
570
- if (currentMode === 'thinking') {
571
- thoughtsBuffer += decoder.decode(buffer, { stream: true });
572
- } else if (currentMode === 'answering') {
573
- answerBuffer += decoder.decode(buffer, { stream: true });
574
- }
575
- buffer = ''; // Clear buffer after processing
576
- }
577
- scheduleUpdate(); // Final display update to render last buffer content
578
- break; // Exit loop
579
  }
580
- // Process the received chunk
581
- await processChunk(value);
582
  }
583
-
584
  } catch (error) {
585
- console.error('Erreur de Fetch ou du Stream:', error);
586
- // Append a user-friendly error message to the answer area if nothing is there, or append to existing content
587
- if(answerContent.innerHTML === '' && answerBuffer === '') { // If solution area is empty
588
- answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue lors du traitement de votre demande: ${error.message}</div>`;
589
- } else { // If some output exists, append the error
590
- answerBuffer += `\n\n<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
591
- scheduleUpdate(); // Update display with the appended error
592
- }
593
- Swal.fire({
594
- icon: 'error',
595
- title: 'Erreur de connexion ou de traitement',
596
- text: `Une erreur est survenue lors du traitement de votre demande. Détails: ${error.message}`
597
- });
598
  } finally {
599
- // Code qui s'exécute après try/catch, qu'il y ait eu une erreur ou non
600
- stopTimer(); // Arrêter le timer de l'analyse
601
- loader.classList.add('hidden'); // Cacher le loader
602
- solutionSection.classList.remove('hidden'); // S'assurer que la section solution est visible
603
-
604
- // --- Mise à jour finale de l'état du bouton ---
605
- updateSubmitButtonState(); // Vérifier si le délai est terminé et mettre à jour le bouton
606
- // --- Fin Mise à jour finale ---
607
  }
608
  });
609
 
610
-
611
- // Sauvegarde de la solution avec SweetAlert2
612
- saveButton.addEventListener('click', () => {
613
- Swal.fire({
614
- title: 'Sauvegarder la solution',
615
  input: 'text',
616
- inputLabel: 'Nom de la sauvegarde',
617
- inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
618
  showCancelButton: true,
619
  confirmButtonText: 'Sauvegarder',
620
  cancelButtonText: 'Annuler',
621
  inputValidator: (value) => {
622
- if (!value) {
623
- return 'Veuillez donner un nom à votre sauvegarde !';
624
- }
625
- }
626
- }).then((result) => {
627
- if (result.isConfirmed && result.value) {
628
- const saveName = result.value;
629
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
630
-
631
- if (savedExercises[saveName]) {
632
- // Ask if user wants to overwrite
633
- Swal.fire({
634
- title: `Sauvegarde "${saveName}" existe déjà. Écraser ?`,
635
- icon: 'warning',
636
- showCancelButton: true,
637
- confirmButtonColor: '#3085d6',
638
- cancelButtonColor: '#d33',
639
- confirmButtonText: 'Oui, écraser',
640
- cancelButtonText: 'Annuler'
641
- }).then((overwriteResult) => {
642
- if (overwriteResult.isConfirmed) {
643
- saveCurrentSolution(saveName, savedExercises);
644
- }
645
- });
646
- } else {
647
- saveCurrentSolution(saveName, savedExercises);
648
- }
649
  }
650
  });
651
- });
652
-
653
- const saveCurrentSolution = (saveName, existingSaves) => {
654
- const saveData = {
655
- answer: answerContent.innerHTML, // Save the rendered HTML
656
- thinking: thoughtsContent.innerHTML, // Save the rendered HTML
657
- date: new Date().toLocaleString()
658
- };
659
- existingSaves[saveName] = saveData;
660
- localStorage.setItem('savedExercises', JSON.stringify(existingSaves));
661
- Swal.fire({
662
- icon: 'success',
663
- title: 'Sauvegarde réussie',
664
- text: 'Votre solution a bien été sauvegardée !',
665
- timer: 2000,
666
- showConfirmButton: false
667
- });
668
- };
669
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
 
671
- // Chargement des sauvegardes dans le modal
672
  const loadSavedList = () => {
673
- savedList.innerHTML = '';
674
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
675
-
676
  if (Object.keys(savedExercises).length === 0) {
677
- savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
678
  return;
679
  }
680
-
681
- // Sort by date, newest first (optional but nice)
682
- const sortedEntries = Object.entries(savedExercises).sort(([,a], [,b]) => new Date(b.date) - new Date(a.date));
683
-
684
-
685
- for (const [name, data] of sortedEntries) {
686
  const li = document.createElement('li');
687
- li.className = 'border-b pb-2';
 
 
 
 
 
688
  li.innerHTML = `
689
- <div class="flex justify-between items-center">
690
- <button class="text-left text-blue-600 hover:underline" data-save="${encodeURIComponent(name)}">
691
- ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
692
- </button>
693
- <button class="text-red-500 hover:text-red-700" data-delete="${encodeURIComponent(name)}">
694
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
695
- <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
696
- </svg>
697
- </button>
698
- </div>
699
  `;
700
- savedList.appendChild(li);
701
  }
702
  };
703
 
704
- // Gestion des clics sur les sauvegardes
705
- savedList.addEventListener('click', (e) => {
706
- // Chargement d'une sauvegarde
707
- if (e.target && e.target.dataset.save) {
708
- const saveName = decodeURIComponent(e.target.dataset.save);
 
 
 
709
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
710
  const data = savedExercises[saveName];
711
  if (data) {
712
- // Hide the form and loader, show the solution section
713
- form.classList.add('hidden');
714
- loader.classList.add('hidden');
715
- solutionSection.classList.remove('hidden');
716
-
717
- // Load the saved HTML content
718
- thoughtsContent.innerHTML = data.thinking;
719
- answerContent.innerHTML = data.answer;
720
-
721
- // Close the modal
722
- savedModal.classList.remove('active');
723
-
724
- // Re-render MathJax and apply highlighting to the loaded HTML
725
- // Use MathJax.typesetPromise() on the specific elements
726
- if (window.mathJaxReady) {
727
- MathJax.startup.document.elements = [thoughtsContent, answerContent]; // Target both potentially
728
- MathJax.typesetPromise().then(() => {
729
- applyHighlighting(); // Apply highlighting after MathJax
730
- }).catch((err) => console.error("MathJax typesetting failed:", err));
731
- } else {
732
- console.warn("MathJax not ready yet, cannot typeset saved content.");
733
- }
734
-
735
- // Ensure thoughts box is open when viewing a saved solution
736
- thoughtsBox.classList.add('open');
737
-
738
- // Reset buffers and timer as this is static content
739
- thoughtsBuffer = ''; // Buffers are for streaming, not needed for static load
740
- answerBuffer = ''; // Buffers are for streaming, not needed for static load
741
- currentMode = null; // Not streaming
742
- stopTimer(); // Stop the live timer
743
- timestamp.textContent = data.date; // Show saved date as timestamp
744
- submitButton.disabled = true; // Disable submit button while viewing a save
745
- submitButton.textContent = `Vue de la sauvegarde: ${saveName}`;
746
  }
747
- }
748
-
749
- // Suppression d'une sauvegarde
750
- if (e.target && (e.target.dataset.delete || e.target.closest('[data-delete]'))) {
751
- const deleteName = decodeURIComponent(e.target.dataset.delete || e.target.closest('[data-delete]').dataset.delete);
752
-
753
  Swal.fire({
754
- title: 'Êtes-vous sûr ?',
755
- text: "Cette sauvegarde sera définitivement supprimée.",
756
  icon: 'warning',
757
  showCancelButton: true,
758
- confirmButtonColor: '#3085d6',
759
- cancelButtonColor: '#d33',
760
- confirmButtonText: 'Oui, supprimer',
761
  cancelButtonText: 'Annuler'
762
  }).then((result) => {
763
  if (result.isConfirmed) {
764
- const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
765
  delete savedExercises[deleteName];
766
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
767
-
768
- Swal.fire(
769
- 'Supprimé !',
770
- 'La sauvegarde a été supprimée.',
771
- 'success'
772
- );
773
-
774
- loadSavedList(); // Refresh the list in the modal
775
- // If the deleted save was being viewed, reset the view
776
- if (submitButton.textContent.includes(`Vue de la sauvegarde: ${deleteName}`)) {
777
- resetToNewExerciseState();
778
- }
779
  }
780
  });
781
  }
782
  });
783
 
784
- // Ouverture / fermeture du modal de sauvegardes
785
- openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
786
- closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
787
-
788
- // Fonction pour réinitialiser l'état pour un nouvel exercice
789
- const resetToNewExerciseState = () => {
790
- // Reset form and hide solution
791
- form.reset();
792
- form.classList.remove('hidden');
793
- solutionSection.classList.add('hidden');
794
- imagePreview.classList.add('hidden'); // Hide image preview
795
- previewImage.src = ''; // Clear image source
796
-
797
- // Clear content areas and buffers
798
- thoughtsContent.innerHTML = '';
799
- answerContent.innerHTML = '';
800
- thoughtsBuffer = '';
801
- answerBuffer = '';
802
- currentMode = null; // Reset mode
803
-
804
- // Stop timer and clear timestamp
805
- stopTimer();
806
- timestamp.textContent = '';
807
-
808
- // Ensure thoughts box is collapsed for a new exercise
809
- thoughtsBox.classList.remove('open');
810
-
811
- // Update button state based on cooldown
812
- updateSubmitButtonState(); // This will enable/disable based on the 3-min rule
813
-
814
- // Close the modal if open
815
- savedModal.classList.remove('active');
816
- };
817
-
818
-
819
- // Bouton présent uniquement dans le modal pour lancer un nouvel exercice
820
- newExercise.addEventListener('click', resetToNewExerciseState);
821
-
822
-
823
- // Initial check for cooldown on page load
824
- updateSubmitButtonState();
825
 
 
 
826
  });
827
  </script>
828
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mariam M-1 | Solution Mathématique Intelligente</title>
7
  <!-- Tailwind CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
 
9
  <!-- SweetAlert2 -->
10
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
 
11
  <!-- Highlight.js pour la coloration syntaxique -->
12
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
 
14
  <!-- Configuration de MathJax -->
15
  <script>
16
  window.MathJax = {
 
44
  .uploadArea:hover { border-color: #3b82f6; }
45
 
46
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
47
+ .blue-button:hover:not(:disabled) { background: #2563eb; }
48
  .blue-button:disabled {
49
+ background: #9ca3af;
50
  cursor: not-allowed;
51
  }
52
 
53
+ .green-button { background: #10b981; transition: background-color 0.2s ease; }
54
+ .green-button:hover:not(:disabled) { background: #059669; }
55
+ .green-button.active { background: #059669; }
56
 
57
  .loader {
58
+ width: 48px; height: 48px; border: 3px solid #3b82f6; border-bottom-color: transparent;
59
+ border-radius: 50%; display: inline-block; animation: rotation 1s linear infinite;
 
 
 
 
 
60
  }
61
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
62
 
63
  .thought-box {
64
+ transition: max-height 0.3s ease-out; max-height: 0; overflow: hidden;
 
 
65
  }
66
  .thought-box.open { max-height: 500px; }
67
 
68
  #thoughtsContent, #answerContent {
69
+ max-height: 500px; overflow-y: auto; scroll-behavior: smooth; white-space: pre-wrap;
 
 
 
70
  }
71
 
72
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
 
73
  .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
74
 
75
  table {
76
+ border-collapse: collapse; width: 100%; margin-bottom: 1rem;
 
 
77
  }
78
  th, td {
79
+ border: 1px solid #d1d5db; padding: 0.5rem; text-align: left;
 
 
80
  }
81
  th { background-color: #f3f4f6; font-weight: 600; }
82
  .table-responsive { overflow-x: auto; }
83
 
 
84
  #saveButton {
85
+ background: #3b82f6; color: white; padding: 0.5rem 1rem;
86
+ border-radius: 0.375rem; transition: background-color 0.2s ease;
 
 
 
87
  }
88
  #saveButton:hover { background: #2563eb; }
89
 
90
  /* Modal plein écran pour les sauvegardes */
91
  #savedModal {
92
+ display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50;
 
 
 
 
93
  }
94
  #savedModal.active { display: block; }
95
  #savedModalContent {
96
+ background: #fff; width: 100%; height: 100%; overflow-y: auto;
 
 
 
97
  }
98
 
99
+ /* Styles pour le code et son exécution */
100
  pre {
101
+ background-color: #f8f8f8; border: 1px solid #e2e8f0; border-radius: 0.375rem;
102
+ padding: 1rem; margin: 1rem 0; overflow-x: auto;
 
 
 
 
103
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
104
  }
105
 
 
108
  }
109
 
110
  .code-execution-result {
111
+ background-color: #f0fff4; border-left: 4px solid #48bb78;
112
+ padding: 1rem; margin: 1rem 0;
 
 
113
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
114
  white-space: pre-wrap;
115
  }
116
 
 
117
  .content-text {}
118
  .content-code { padding: 0; }
119
  .content-result {
120
+ background-color: #f0fff4; border-left: 4px solid #48bb78;
121
+ padding: 1rem; margin: 0.5rem 0;
 
 
122
  }
123
  .content-image { margin: 1rem 0; text-align: center; }
124
  .content-image img { max-width: 100%; }
125
 
126
+ /* Style pour l'option calculatrice */
127
+ .calculator-option {
128
+ background: #f8fafc; border: 2px solid #e2e8f0; border-radius: 0.5rem;
129
+ padding: 1rem; margin: 1rem 0; transition: all 0.2s ease;
130
+ }
131
+ .calculator-option.enabled {
132
+ background: #ecfdf5; border-color: #10b981;
133
+ }
134
+
135
+ .toggle-switch {
136
+ position: relative; display: inline-block; width: 60px; height: 34px;
137
+ }
138
+ .toggle-switch input {
139
+ opacity: 0; width: 0; height: 0;
140
+ }
141
+ .slider {
142
+ position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
143
+ background-color: #ccc; transition: .4s; border-radius: 34px;
144
+ }
145
+ .slider:before {
146
+ position: absolute; content: ""; height: 26px; width: 26px;
147
+ left: 4px; bottom: 4px; background-color: white;
148
+ transition: .4s; border-radius: 50%;
149
+ }
150
+ input:checked + .slider {
151
+ background-color: #10b981;
152
+ }
153
+ input:checked + .slider:before {
154
+ transform: translateX(26px);
155
+ }
156
  </style>
157
  </head>
158
  <body class="p-4">
159
  <div class="max-w-4xl mx-auto">
160
  <header class="p-6 text-center mb-8">
161
+ <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
162
+ <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
163
+
164
  <div class="mt-4 flex justify-end">
165
  <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
166
  </div>
 
182
  <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
183
  </div>
184
  </div>
185
+
186
  <!-- Aperçu de l'image -->
187
  <div id="imagePreview" class="hidden text-center">
188
  <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
189
  </div>
190
+
191
+ <!-- Option Calculatrice -->
192
+ <div id="calculatorOption" class="calculator-option">
193
+ <div class="flex items-center justify-between">
194
+ <div class="flex items-center space-x-3">
195
+ <div class="text-2xl">🧮</div>
196
+ <div>
197
+ <h3 class="font-semibold text-gray-800">Mode Calculatrice</h3>
198
+ <p class="text-sm text-gray-600">Active l'exécution de code Python pour les calculs numériques et graphiques</p>
199
+ </div>
200
+ </div>
201
+ <label class="toggle-switch">
202
+ <input type="checkbox" id="calculatorToggle">
203
+ <span class="slider"></span>
204
+ </label>
205
+ </div>
206
+
207
+ <div id="calculatorFeatures" class="mt-3 text-xs text-gray-500 hidden">
208
+ <div class="grid grid-cols-2 gap-2">
209
+ <div class="flex items-center space-x-1">
210
+ <span>✓</span><span>Calculs numériques précis</span>
211
+ </div>
212
+ <div class="flex items-center space-x-1">
213
+ <span>✓</span><span>Graphiques matplotlib</span>
214
+ </div>
215
+ <div class="flex items-center space-x-1">
216
+ <span>✓</span><span>Vérification des résultats</span>
217
+ </div>
218
+ <div class="flex items-center space-x-1">
219
+ <span>✓</span><span>Visualisations 2D/3D</span>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
  <!-- Le bouton de soumission -->
226
  <button type="submit" id="submitButton" class="blue-button w-full py-3 text-white font-medium rounded-lg">
227
  Résoudre le problème
 
237
  <!-- Zone d'affichage de la solution -->
238
  <section id="solution" class="hidden mt-8 space-y-6 relative">
239
  <div class="border-t pt-4">
240
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2 hover:bg-gray-100 rounded">
241
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
242
  <span id="timestamp" class="timestamp"></span>
243
  </button>
 
248
  <div class="border-t pt-6">
249
  <div class="flex justify-between items-center">
250
  <h3 class="text-xl font-bold text-gray-800 mb-4">Solution</h3>
 
251
  <button id="saveButton">Sauvegarder</button>
252
  </div>
253
  <div id="answerContent" class="text-gray-700 table-responsive"></div>
 
261
  <div id="savedModalContent" class="p-6">
262
  <header class="flex justify-between items-center border-b pb-4">
263
  <h2 class="text-2xl font-bold">Sauvegardes</h2>
264
+ <button id="closeSaved" class="text-3xl text-gray-600 hover:text-gray-800">×</button>
265
  </header>
266
  <div id="savedListContainer" class="mt-4">
267
  <ul id="savedList" class="space-y-4">
 
279
  <script>
280
  document.addEventListener('DOMContentLoaded', () => {
281
  // Récupération des éléments
282
+ const elements = {
283
+ form: document.getElementById('problemForm'),
284
+ imageInput: document.getElementById('imageInput'),
285
+ submitButton: document.getElementById('submitButton'),
286
+ loader: document.getElementById('loader'),
287
+ solutionSection: document.getElementById('solution'),
288
+ thoughtsContent: document.getElementById('thoughtsContent'),
289
+ answerContent: document.getElementById('answerContent'),
290
+ thoughtsToggle: document.getElementById('thoughtsToggle'),
291
+ thoughtsBox: document.getElementById('thoughtsBox'),
292
+ imagePreview: document.getElementById('imagePreview'),
293
+ previewImage: document.getElementById('previewImage'),
294
+ timestamp: document.getElementById('timestamp'),
295
+ saveButton: document.getElementById('saveButton'),
296
+ openSaved: document.getElementById('openSaved'),
297
+ closeSaved: document.getElementById('closeSaved'),
298
+ savedModal: document.getElementById('savedModal'),
299
+ savedList: document.getElementById('savedList'),
300
+ newExercise: document.getElementById('newExercise'),
301
+ calculatorToggle: document.getElementById('calculatorToggle'),
302
+ calculatorOption: document.getElementById('calculatorOption'),
303
+ calculatorFeatures: document.getElementById('calculatorFeatures'),
304
+ dropZone: document.querySelector('.uploadArea')
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  };
306
 
307
+ // État de l'application
308
+ const state = {
309
+ startTime: null,
310
+ timerInterval: null,
311
+ cooldownTimerInterval: null,
312
+ thoughtsBuffer: '',
313
+ answerBuffer: '',
314
+ currentMode: null,
315
+ updateTimeout: null,
316
+ selectedFile: null
317
  };
318
 
319
+ // Configuration
320
+ const COOLDOWN_DURATION_MS = 3 * 60 * 1000; // 3 minutes
321
+ const LAST_SUBMISSION_TIME_KEY = 'mariamM1_lastSubmissionTime';
322
+ const SUBMIT_BUTTON_ORIGINAL_TEXT = 'Résoudre le problème';
 
 
 
 
 
 
 
323
 
324
+ marked.setOptions({
325
+ gfm: true,
326
+ breaks: true,
327
+ highlight: function(code, lang) {
328
+ if (lang && hljs.getLanguage(lang)) {
329
+ try {
330
+ return hljs.highlight(code, { language: lang }).value;
331
+ } catch (error) {
332
+ console.error("Highlighting error:", error);
333
+ return code;
334
+ }
335
  }
336
+ try {
337
+ return hljs.highlightAuto(code).value;
338
+ } catch (error) {
339
+ console.error("Auto highlighting error:", error);
340
+ return code;
 
 
 
341
  }
342
  }
343
+ });
344
+
345
+ // --- Helper Functions ---
346
+ const formatTime = (totalSeconds) => {
347
+ const minutes = Math.floor(totalSeconds / 60);
348
+ const seconds = totalSeconds % 60;
349
+ return `${minutes}m ${seconds < 10 ? '0' : ''}${seconds}s`;
350
+ };
351
+
352
+ const updateTimestampDisplay = () => {
353
+ if (state.startTime) {
354
+ const seconds = Math.floor((Date.now() - state.startTime) / 1000);
355
+ elements.timestamp.textContent = `${seconds}s`;
356
+ }
357
+ };
358
+
359
+ const startSolutionTimer = () => {
360
+ state.startTime = Date.now();
361
+ if (state.timerInterval) clearInterval(state.timerInterval);
362
+ state.timerInterval = setInterval(updateTimestampDisplay, 1000);
363
+ updateTimestampDisplay();
364
+ };
365
+
366
+ const stopSolutionTimer = () => {
367
+ clearInterval(state.timerInterval);
368
+ state.timerInterval = null;
369
  };
370
 
371
+ const resetSolutionTimer = () => {
372
+ stopSolutionTimer();
373
+ state.startTime = null;
374
+ elements.timestamp.textContent = '';
375
+ };
376
 
377
+ // --- Cooldown Logic ---
378
+ const getLastSubmissionTime = () => parseInt(localStorage.getItem(LAST_SUBMISSION_TIME_KEY) || '0');
379
+ const setLastSubmissionTime = () => localStorage.setItem(LAST_SUBMISSION_TIME_KEY, Date.now().toString());
380
 
381
+ const updateSubmitButtonState = () => {
382
+ const lastSubmission = getLastSubmissionTime();
383
+ const now = Date.now();
384
+ const timeSinceLastSubmission = now - lastSubmission;
385
+
386
+ if (state.cooldownTimerInterval) clearInterval(state.cooldownTimerInterval);
387
+
388
+ if (timeSinceLastSubmission < COOLDOWN_DURATION_MS) {
389
+ elements.submitButton.disabled = true;
390
+ let remainingTimeMs = COOLDOWN_DURATION_MS - timeSinceLastSubmission;
391
+
392
+ const updateButtonText = () => {
393
+ const remainingSeconds = Math.ceil(remainingTimeMs / 1000);
394
+ elements.submitButton.textContent = `Attendre ${formatTime(remainingSeconds)}`;
395
+ remainingTimeMs -= 1000;
396
+ if (remainingTimeMs < 0) {
397
+ clearInterval(state.cooldownTimerInterval);
398
+ state.cooldownTimerInterval = null;
399
+ elements.submitButton.disabled = false;
400
+ elements.submitButton.textContent = SUBMIT_BUTTON_ORIGINAL_TEXT;
401
+ }
402
+ };
403
+ updateButtonText();
404
+ state.cooldownTimerInterval = setInterval(updateButtonText, 1000);
405
+ } else {
406
+ elements.submitButton.disabled = false;
407
+ elements.submitButton.textContent = SUBMIT_BUTTON_ORIGINAL_TEXT;
408
  }
409
  };
 
 
410
 
411
+ // --- Calculator Toggle ---
412
+ elements.calculatorToggle.addEventListener('change', () => {
413
+ const isEnabled = elements.calculatorToggle.checked;
414
+ if (isEnabled) {
415
+ elements.calculatorOption.classList.add('enabled');
416
+ elements.calculatorFeatures.classList.remove('hidden');
417
+ } else {
418
+ elements.calculatorOption.classList.remove('enabled');
419
+ elements.calculatorFeatures.classList.add('hidden');
420
+ }
421
+ });
422
+
423
+ // --- File Handling ---
424
+ const handleFileSelect = (file) => {
425
+ if (!file || !file.type.startsWith('image/')) {
426
+ Swal.fire('Fichier Invalide', 'Veuillez sélectionner un fichier image.', 'error');
427
+ elements.imageInput.value = '';
428
+ state.selectedFile = null;
429
+ elements.imagePreview.classList.add('hidden');
430
+ return;
431
+ }
432
+ state.selectedFile = file;
433
  const reader = new FileReader();
434
  reader.onload = e => {
435
+ elements.previewImage.src = e.target.result;
436
+ elements.imagePreview.classList.remove('hidden');
437
  };
438
  reader.readAsDataURL(file);
439
  };
440
 
441
+ // --- MathJax & Display Update ---
442
+ const typesetMathJaxContent = async (contentElement) => {
443
+ if (window.mathJaxReady && contentElement) {
444
+ MathJax.startup.document.elements = [contentElement];
445
+ try {
446
+ await MathJax.typesetPromise();
447
+ contentElement.scrollTop = contentElement.scrollHeight;
448
+ } catch(err) {
449
+ console.error("MathJax typesetting error:", err);
450
+ }
451
+ } else if (contentElement) {
452
+ setTimeout(() => typesetMathJaxContent(contentElement), 200);
453
+ }
454
+ };
455
 
 
456
  const applyHighlighting = () => {
457
  document.querySelectorAll('pre code').forEach((block) => {
458
  hljs.highlightBlock(block);
459
  });
460
  };
461
 
462
+ const scheduleDisplayUpdate = () => {
463
+ if (state.updateTimeout) clearTimeout(state.updateTimeout);
464
+ state.updateTimeout = setTimeout(async () => {
465
+ if (elements.thoughtsContent) elements.thoughtsContent.innerHTML = marked.parse(state.thoughtsBuffer);
466
+ if (elements.answerContent) elements.answerContent.innerHTML = marked.parse(state.answerBuffer);
467
+
468
+ if (state.thoughtsBuffer) await typesetMathJaxContent(elements.thoughtsContent);
469
+ if (state.answerBuffer) await typesetMathJaxContent(elements.answerContent);
470
+
471
+ applyHighlighting();
472
+
473
+ state.updateTimeout = null;
474
+ }, 150);
475
  };
476
 
477
+ const resetUIForNewProblem = () => {
478
+ elements.form.reset();
479
+ elements.imageInput.value = '';
480
+ state.selectedFile = null;
481
+ elements.imagePreview.classList.add('hidden');
482
+ elements.previewImage.src = '#';
483
+ elements.solutionSection.classList.add('hidden');
484
+ elements.loader.classList.add('hidden');
485
+ elements.thoughtsContent.innerHTML = '';
486
+ elements.answerContent.innerHTML = '';
487
+ state.thoughtsBuffer = '';
488
+ state.answerBuffer = '';
489
+ state.currentMode = null;
490
+ elements.thoughtsBox.classList.remove('open');
491
+ elements.form.classList.remove('hidden');
492
+ resetSolutionTimer();
493
+ updateSubmitButtonState();
494
+ // Reset calculator toggle
495
+ elements.calculatorToggle.checked = false;
496
+ elements.calculatorOption.classList.remove('enabled');
497
+ elements.calculatorFeatures.classList.add('hidden');
498
  };
499
 
500
+ // --- Event Listeners ---
501
+ elements.thoughtsToggle.addEventListener('click', () => elements.thoughtsBox.classList.toggle('open'));
502
+ elements.imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
503
 
504
+ // Drag & Drop
505
+ elements.dropZone.addEventListener('dragover', e => {
506
+ e.preventDefault();
507
+ elements.dropZone.classList.add('border-blue-400');
508
+ });
509
+ elements.dropZone.addEventListener('dragleave', e => {
510
+ e.preventDefault();
511
+ elements.dropZone.classList.remove('border-blue-400');
512
+ });
513
+ elements.dropZone.addEventListener('drop', e => {
514
+ e.preventDefault();
515
+ elements.dropZone.classList.remove('border-blue-400');
516
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
517
+ handleFileSelect(e.dataTransfer.files[0]);
518
+ elements.imageInput.files = e.dataTransfer.files;
 
 
 
 
519
  }
520
  });
521
 
522
+ // Form submission
523
+ elements.form.addEventListener('submit', async e => {
524
+ e.preventDefault();
525
+
526
+ if (!state.selectedFile) {
527
+ Swal.fire('Aucune Image', 'Veuillez sélectionner une image.', 'warning');
528
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  }
 
530
 
531
+ const lastSubmission = getLastSubmissionTime();
532
+ const now = Date.now();
533
+ if (now - lastSubmission < COOLDOWN_DURATION_MS) {
534
+ const remainingSeconds = Math.ceil((COOLDOWN_DURATION_MS - (now - lastSubmission)) / 1000);
535
+ Swal.fire('Cooldown Actif', `Vous devez attendre ${formatTime(remainingSeconds)} avant de soumettre à nouveau.`, 'info');
 
 
 
 
 
536
  return;
537
  }
538
 
539
+ setLastSubmissionTime();
540
+ updateSubmitButtonState();
 
 
541
 
542
+ startSolutionTimer();
543
+ elements.loader.classList.remove('hidden');
544
+ elements.solutionSection.classList.add('hidden');
545
+ elements.thoughtsContent.innerHTML = '';
546
+ elements.answerContent.innerHTML = '';
547
+ state.thoughtsBuffer = '';
548
+ state.answerBuffer = '';
549
+ state.currentMode = null;
550
+ elements.thoughtsBox.classList.add('open');
 
 
551
 
552
  const formData = new FormData();
553
+ formData.append('image', state.selectedFile);
554
+ formData.append('use_calculator', elements.calculatorToggle.checked.toString());
555
 
556
  try {
557
+ const response = await fetch('/solve', { method: 'POST', body: formData });
 
 
558
  if (!response.ok) {
559
+ throw new Error(`HTTP error! status: ${response.status}`);
 
 
560
  }
 
 
 
 
 
561
  const reader = response.body.getReader();
562
  const decoder = new TextDecoder();
563
+ let streamBuffer = '';
564
 
565
+ while (true) {
566
+ const { done, value } = await reader.read();
567
+ if (done) {
568
+ if (streamBuffer.startsWith('data:')) {
569
+ try {
570
+ const data = JSON.parse(streamBuffer.slice(5));
571
+ if (data.content) {
572
+ if (state.currentMode === 'thinking') state.thoughtsBuffer += data.content;
573
+ else if (state.currentMode === 'answering') state.answerBuffer += data.content;
574
+ }
575
+ } catch (parseError) {
576
+ console.warn("Error parsing final chunk:", parseError, "Buffer:", streamBuffer);
577
+ }
578
  }
579
+ scheduleDisplayUpdate();
580
+ break;
581
+ }
582
 
583
+ streamBuffer += decoder.decode(value, { stream: true });
584
+ const parts = streamBuffer.split('\n\n');
585
+ streamBuffer = parts.pop();
 
 
 
586
 
587
+ for (const part of parts) {
588
+ if (!part.startsWith('data:')) continue;
589
+ try {
590
+ const jsonData = part.slice(5);
591
+ const data = JSON.parse(jsonData);
592
+
593
+ if (data.mode) {
594
+ state.currentMode = data.mode;
595
+ if (!elements.loader.classList.contains('hidden')) {
596
+ elements.loader.classList.add('hidden');
597
+ elements.solutionSection.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  }
599
+ }
600
+
601
+ if (data.content) {
602
+ if (state.currentMode === 'thinking') {
603
+ state.thoughtsBuffer += data.content;
604
+ } else if (state.currentMode === 'answering') {
605
+ switch(data.type) {
606
+ case 'code':
607
+ state.answerBuffer += "\n```python\n" + data.content + "\n```\n";
608
+ break;
609
+ case 'result':
610
+ const formattedResult = data.content.split('\n').map(line => `> ${line}`).join('\n');
611
+ state.answerBuffer += "\n" + formattedResult + "\n";
612
+ break;
613
+ case 'image':
614
+ state.answerBuffer += `\n![Résultat](data:image/png;base64,${data.content})\n`;
615
+ break;
616
+ case 'text':
617
+ default:
618
+ state.answerBuffer += data.content;
619
+ break;
620
  }
621
  }
622
+ }
623
 
624
+ if (data.error) {
625
+ if (state.currentMode === 'thinking') {
626
+ state.thoughtsBuffer += `\n**Erreur pendant la réflexion:** ${data.error}\n`;
627
+ } else {
628
+ state.answerBuffer += `\n**Erreur:** ${data.error}\n`;
629
+ }
630
  }
 
 
 
631
 
632
+ } catch (e) {
633
+ console.error('Error parsing JSON data:', part.slice(5), e);
634
+ if (state.currentMode === 'thinking') {
635
+ state.thoughtsBuffer += `\n[Erreur de traitement du flux]`;
636
+ } else {
637
+ state.answerBuffer += `\n[Erreur de traitement du flux]`;
638
+ }
639
+ }
 
 
 
 
 
 
 
 
 
640
  }
641
+ scheduleDisplayUpdate();
 
642
  }
 
643
  } catch (error) {
644
+ console.error('Erreur de soumission:', error);
645
+ Swal.fire('Erreur', `Une erreur est survenue lors de la résolution: ${error.message}`, 'error');
646
+ elements.loader.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
647
  } finally {
648
+ stopSolutionTimer();
 
 
 
 
 
 
 
649
  }
650
  });
651
 
652
+ // --- Saved Solutions Logic ---
653
+ elements.saveButton.addEventListener('click', async () => {
654
+ const { value: saveName } = await Swal.fire({
655
+ title: 'Nom de la sauvegarde',
 
656
  input: 'text',
657
+ inputPlaceholder: 'Ex: Exercice Maths Ch.3',
 
658
  showCancelButton: true,
659
  confirmButtonText: 'Sauvegarder',
660
  cancelButtonText: 'Annuler',
661
  inputValidator: (value) => {
662
+ if (!value) return 'Vous devez entrer un nom !';
 
 
 
 
 
 
663
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
664
+ if (savedExercises[value]) return 'Ce nom existe déjà. Choisissez-en un autre.';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  }
666
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
 
668
+ if (saveName) {
669
+ const saveData = {
670
+ answer: elements.answerContent.innerHTML,
671
+ thinking: elements.thoughtsContent.innerHTML,
672
+ date: new Date().toLocaleString('fr-FR'),
673
+ calculatorUsed: elements.calculatorToggle.checked
674
+ };
675
+ let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
676
+ savedExercises[saveName] = saveData;
677
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
678
+ Swal.fire('Sauvegardé!', 'Votre solution a été sauvegardée.', 'success');
679
+ }
680
+ });
681
 
 
682
  const loadSavedList = () => {
683
+ elements.savedList.innerHTML = '';
684
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
 
685
  if (Object.keys(savedExercises).length === 0) {
686
+ elements.savedList.innerHTML = '<li class="text-gray-500">Aucune sauvegarde pour le moment.</li>';
687
  return;
688
  }
689
+
690
+ for (const [name, data] of Object.entries(savedExercises).sort((a,b) => new Date(b[1].date) - new Date(a[1].date))) {
 
 
 
 
691
  const li = document.createElement('li');
692
+ li.className = 'flex justify-between items-center p-2 hover:bg-gray-100 rounded';
693
+
694
+ const calculatorBadge = data.calculatorUsed ?
695
+ '<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 ml-2">🧮 Calculatrice</span>' :
696
+ '';
697
+
698
  li.innerHTML = `
699
+ <button class="text-left text-blue-600 hover:underline focus:outline-none" data-save-name="${name}">
700
+ ${name} ${calculatorBadge}<br>
701
+ <span class="text-gray-500 text-xs">(${data.date})</span>
702
+ </button>
703
+ <button class="text-red-500 hover:text-red-700 text-xs p-1 focus:outline-none" data-delete-name="${name}" aria-label="Supprimer ${name}">
704
+ Supprimer
705
+ </button>
 
 
 
706
  `;
707
+ elements.savedList.appendChild(li);
708
  }
709
  };
710
 
711
+ elements.savedList.addEventListener('click', (e) => {
712
+ const target = e.target.closest('button');
713
+ if (!target) return;
714
+
715
+ const saveName = target.dataset.saveName;
716
+ const deleteName = target.dataset.deleteName;
717
+
718
+ if (saveName) {
719
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
720
  const data = savedExercises[saveName];
721
  if (data) {
722
+ resetUIForNewProblem();
723
+ elements.form.classList.add('hidden');
724
+ elements.loader.classList.add('hidden');
725
+ elements.solutionSection.classList.remove('hidden');
726
+ elements.thoughtsContent.innerHTML = data.thinking;
727
+ elements.answerContent.innerHTML = data.answer;
728
+ state.thoughtsBuffer = '';
729
+ state.answerBuffer = '';
730
+ typesetMathJaxContent(elements.thoughtsContent).then(() => {
731
+ typesetMathJaxContent(elements.answerContent).then(() => {
732
+ applyHighlighting();
733
+ });
734
+ });
735
+ elements.thoughtsBox.classList.add('open');
736
+ elements.savedModal.classList.remove('active');
737
+ resetSolutionTimer();
738
+ elements.timestamp.textContent = data.date;
739
+
740
+ // Restore calculator state
741
+ if (data.calculatorUsed) {
742
+ elements.calculatorToggle.checked = true;
743
+ elements.calculatorOption.classList.add('enabled');
744
+ elements.calculatorFeatures.classList.remove('hidden');
745
+ }
 
 
 
 
 
 
 
 
 
 
746
  }
747
+ } else if (deleteName) {
 
 
 
 
 
748
  Swal.fire({
749
+ title: `Supprimer "${deleteName}" ?`,
750
+ text: "Cette action est irréversible.",
751
  icon: 'warning',
752
  showCancelButton: true,
753
+ confirmButtonColor: '#d33',
754
+ cancelButtonColor: '#3085d6',
755
+ confirmButtonText: 'Oui, supprimer !',
756
  cancelButtonText: 'Annuler'
757
  }).then((result) => {
758
  if (result.isConfirmed) {
759
+ let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
760
  delete savedExercises[deleteName];
761
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
762
+ loadSavedList();
763
+ Swal.fire('Supprimé!', `"${deleteName}" a été supprimé.`, 'success');
 
 
 
 
 
 
 
 
 
 
764
  }
765
  });
766
  }
767
  });
768
 
769
+ elements.openSaved.addEventListener('click', () => {
770
+ loadSavedList();
771
+ elements.savedModal.classList.add('active');
772
+ });
773
+
774
+ elements.closeSaved.addEventListener('click', () => {
775
+ elements.savedModal.classList.remove('active');
776
+ });
777
+
778
+ elements.newExercise.addEventListener('click', () => {
779
+ resetUIForNewProblem();
780
+ elements.savedModal.classList.remove('active');
781
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
 
783
+ // --- Initialization ---
784
+ resetUIForNewProblem();
785
  });
786
  </script>
787
  </body>