Docfile commited on
Commit
c35dcb1
·
verified ·
1 Parent(s): c9c813b

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +536 -327
templates/index.html CHANGED
@@ -6,7 +6,7 @@
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
 
@@ -25,7 +25,8 @@
25
  },
26
  options: {
27
  enableMenu: false,
28
- messageStyle: 'none'
 
29
  },
30
  startup: {
31
  pageReady: () => { window.mathJaxReady = true; }
@@ -38,17 +39,17 @@
38
  <style>
39
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
40
  body { font-family: 'Space Grotesk', sans-serif; }
41
-
42
  .uploadArea {
43
  background: #f3f4f6;
44
  border: 2px dashed #d1d5db;
45
  transition: border-color 0.2s ease;
46
  }
47
  .uploadArea:hover { border-color: #3b82f6; }
48
-
49
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
50
  .blue-button:hover { background: #2563eb; }
51
-
52
  .loader {
53
  width: 48px;
54
  height: 48px;
@@ -59,29 +60,39 @@
59
  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;
65
  max-height: 0;
66
  overflow: hidden;
67
  }
68
  .thought-box.open { max-height: 500px; }
69
-
70
  #thoughtsContent, #answerContent {
71
- max-height: 500px;
72
  overflow-y: auto;
73
  scroll-behavior: smooth;
74
- white-space: pre-wrap;
 
 
 
 
 
 
 
 
 
75
  }
76
-
77
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
78
-
79
  .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
80
-
81
  table {
82
  border-collapse: collapse;
83
  width: 100%;
84
  margin-bottom: 1rem;
 
85
  }
86
  th, td {
87
  border: 1px solid #d1d5db;
@@ -89,19 +100,17 @@
89
  text-align: left;
90
  }
91
  th { background-color: #f3f4f6; font-weight: 600; }
92
- .table-responsive { overflow-x: auto; }
93
-
94
- /* Style pour le bouton Sauvegarder afin de le mettre en évidence */
95
  #saveButton {
96
- background: #3b82f6;
97
  color: white;
98
  padding: 0.5rem 1rem;
99
  border-radius: 0.375rem;
100
  transition: background-color 0.2s ease;
101
  }
102
- #saveButton:hover { background: #2563eb; }
103
-
104
- /* Modal plein écran pour les sauvegardes */
105
  #savedModal {
106
  display: none;
107
  position: fixed;
@@ -109,13 +118,30 @@
109
  background: rgba(0,0,0,0.5);
110
  z-index: 50;
111
  }
112
- #savedModal.active { display: block; }
113
  #savedModalContent {
114
  background: #fff;
115
- width: 100%;
116
- height: 100%;
 
117
  overflow-y: auto;
 
 
 
118
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  /* Styles spécifiques pour le code et son exécution */
121
  pre {
@@ -123,35 +149,47 @@
123
  border: 1px solid #e2e8f0;
124
  border-radius: 0.375rem;
125
  padding: 1rem;
126
- margin: 1rem 0;
127
  overflow-x: auto;
128
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
 
129
  }
130
-
131
  code {
132
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
 
 
 
133
  }
134
-
 
 
 
 
 
 
135
  .code-execution-result {
136
- background-color: #f0fff4;
137
  border-left: 4px solid #48bb78;
138
  padding: 1rem;
139
- margin: 1rem 0;
140
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
141
- white-space: pre-wrap;
 
142
  }
143
-
144
  /* Styles pour les types de contenu spécifiques */
145
- .content-text {}
146
- .content-code { padding: 0; }
147
- .content-result {
148
- background-color: #f0fff4;
149
- border-left: 4px solid #48bb78;
150
- padding: 1rem;
151
- margin: 0.5rem 0;
152
- }
153
  .content-image { margin: 1rem 0; text-align: center; }
154
- .content-image img { max-width: 100%; }
 
 
 
 
 
 
155
 
156
  /* Onglets pour choisir le modèle */
157
  .tabs {
@@ -159,32 +197,57 @@
159
  border-bottom: 1px solid #e2e8f0;
160
  margin-bottom: 1rem;
161
  }
162
-
163
  .tab {
164
  padding: 0.5rem 1rem;
165
  cursor: pointer;
166
  border-bottom: 2px solid transparent;
 
 
 
 
 
167
  }
168
-
169
  .tab.active {
170
  border-bottom: 2px solid #3b82f6;
171
  color: #3b82f6;
172
  font-weight: 600;
173
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </style>
175
  </head>
176
- <body class="p-4">
177
- <div class="max-w-4xl mx-auto">
178
- <header class="p-6 text-center mb-8">
179
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
180
  <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
181
  <div class="mt-4 flex justify-end">
182
- <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
 
 
 
 
 
183
  </div>
184
  </header>
185
 
186
- <main id="mainContent">
187
- <form id="problemForm" class="space-y-6" novalidate>
188
  <!-- Onglets pour choisir le modèle -->
189
  <div class="tabs">
190
  <div class="tab active" data-endpoint="solve">Gemini Pro</div>
@@ -192,11 +255,11 @@
192
  </div>
193
 
194
  <!-- Zone de dépôt / sélection d'image -->
195
- <div class="uploadArea p-8 text-center relative" aria-label="Zone de dépôt d'image">
196
  <input type="file" id="imageInput" name="image" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
197
  <div class="space-y-3">
198
- <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
199
- <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
200
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
201
  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" />
202
  </svg>
@@ -204,12 +267,16 @@
204
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
205
  <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
206
  </div>
 
 
 
 
207
  </div>
208
- <!-- Aperçu de l'image -->
209
- <div id="imagePreview" class="hidden text-center">
210
- <img id="previewImage" class="preview-image mx-auto" alt="Prévisualisation de l'image">
211
- </div>
212
- <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
213
  Résoudre le problème
214
  </button>
215
  </form>
@@ -221,43 +288,61 @@
221
  </div>
222
 
223
  <!-- Zone d'affichage de la solution -->
224
- <section id="solution" class="hidden mt-8 space-y-6 relative">
225
- <div class="border-t pt-4">
226
- <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
 
 
227
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
228
- <span id="timestamp" class="timestamp"></span>
 
 
 
 
 
229
  </button>
230
- <div id="thoughtsBox" class="thought-box">
231
- <div id="thoughtsContent" class="p-4 text-gray-600"></div>
232
  </div>
233
  </div>
234
- <div class="border-t pt-6">
235
- <div class="flex justify-between items-center">
236
- <h3 class="text-xl font-bold text-gray-800 mb-4">Solution</h3>
237
- <!-- Bouton Sauvegarder mis en évidence -->
238
- <button id="saveButton">Sauvegarder</button>
 
 
 
 
 
 
239
  </div>
240
- <div id="answerContent" class="text-gray-700 table-responsive"></div>
241
  </div>
 
242
  </section>
243
  </main>
244
  </div>
245
 
246
  <!-- Modal plein écran pour les sauvegardes -->
247
  <div id="savedModal">
248
- <div id="savedModalContent" class="p-6">
249
- <header class="flex justify-between items-center border-b pb-4">
250
- <h2 class="text-2xl font-bold">Sauvegardes</h2>
251
- <button id="closeSaved" class="text-3xl text-gray-600">&times;</button>
252
  </header>
253
- <div id="savedListContainer" class="mt-4">
254
  <ul id="savedList" class="space-y-4">
255
  <!-- Liste des sauvegardes insérée dynamiquement -->
 
256
  </ul>
257
  </div>
258
- <div class="mt-6">
259
- <button id="newExercise" class="blue-button w-full py-3 text-white font-medium rounded-lg">
260
- Résoudre un nouvel exercice
 
 
 
261
  </button>
262
  </div>
263
  </div>
@@ -265,7 +350,7 @@
265
 
266
  <script>
267
  document.addEventListener('DOMContentLoaded', () => {
268
- // Récupération des éléments
269
  const form = document.getElementById('problemForm');
270
  const imageInput = document.getElementById('imageInput');
271
  const loader = document.getElementById('loader');
@@ -274,6 +359,7 @@
274
  const answerContent = document.getElementById('answerContent');
275
  const thoughtsToggle = document.getElementById('thoughtsToggle');
276
  const thoughtsBox = document.getElementById('thoughtsBox');
 
277
  const imagePreview = document.getElementById('imagePreview');
278
  const previewImage = document.getElementById('previewImage');
279
  const timestamp = document.getElementById('timestamp');
@@ -285,16 +371,34 @@
285
  const newExercise = document.getElementById('newExercise');
286
  const mainContent = document.getElementById('mainContent');
287
  const tabs = document.querySelectorAll('.tab');
 
288
 
 
289
  let startTime = null;
290
  let timerInterval = null;
291
- let thoughtsBuffer = '';
292
- let answerBuffer = '';
293
  let currentMode = null;
294
- let updateTimeout = null;
295
  let currentEndpoint = 'solve'; // Par défaut: 'solve' pour Gemini Pro
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
- // Gestion des onglets pour le choix du modèle
298
  tabs.forEach(tab => {
299
  tab.addEventListener('click', () => {
300
  tabs.forEach(t => t.classList.remove('active'));
@@ -303,7 +407,7 @@
303
  });
304
  });
305
 
306
- // Mise à jour du temps écoulé
307
  const updateTimestamp = () => {
308
  if (startTime) {
309
  const seconds = Math.floor((Date.now() - startTime) / 1000);
@@ -311,319 +415,404 @@
311
  }
312
  };
313
  const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
314
- const stopTimer = () => { clearInterval(timerInterval); startTime = null; timestamp.textContent = ''; };
315
 
316
- // Affichage de l'image sélectionnée
317
  const handleFileSelect = file => {
318
- if (!file) return;
 
 
 
 
 
319
  const reader = new FileReader();
320
  reader.onload = e => {
321
  previewImage.src = e.target.result;
322
  imagePreview.classList.remove('hidden');
323
  };
324
  reader.readAsDataURL(file);
 
325
  };
326
 
327
- thoughtsToggle.addEventListener('click', () => { thoughtsBox.classList.toggle('open'); });
328
- imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
329
-
330
- // Gestion du glisser-déposer
331
- const dropZone = document.querySelector('.uploadArea');
332
- dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400'); });
333
- dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); });
334
- dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); handleFileSelect(e.dataTransfer.files[0]); });
335
-
336
- // Fonction pour appliquer la coloration syntaxique
337
- const applyHighlighting = () => {
338
- document.querySelectorAll('pre code').forEach((block) => {
339
- hljs.highlightBlock(block);
340
- });
341
- };
342
 
343
- // Rendu MathJax et mise à jour de l'affichage
344
- const typesetAnswerIfReady = async () => {
345
- if (window.mathJaxReady) {
346
- MathJax.startup.document.elements = [document.getElementById('answerContent')];
347
- await MathJax.typesetPromise();
348
- applyHighlighting();
349
- answerContent.scrollTop = answerContent.scrollHeight;
350
- } else { setTimeout(typesetAnswerIfReady, 200); }
351
- };
 
 
 
 
 
 
 
 
352
 
353
- const updateDisplay = async () => {
354
- thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
355
- answerContent.innerHTML = marked.parse(answerBuffer);
356
- await typesetAnswerIfReady();
357
- updateTimeout = null;
358
- };
359
 
360
- const scheduleUpdate = () => { if (!updateTimeout) updateTimeout = setTimeout(updateDisplay, 200); };
 
 
 
 
 
 
 
 
 
361
 
362
- marked.setOptions({
363
- gfm: true,
364
- breaks: true,
365
- highlight: function(code, lang) {
366
- if (lang && hljs.getLanguage(lang)) {
367
- try {
368
- return hljs.highlight(code, { language: lang }).value;
369
- } catch (error) {}
370
- }
371
- return code;
372
- }
373
- });
374
 
375
- // Création d'éléments pour différents types de contenu
376
- const createContentElement = (content, type) => {
377
- const div = document.createElement('div');
378
- div.className = `content-${type}`;
379
-
380
- switch(type) {
381
- case 'text':
382
- div.innerHTML = marked.parse(content);
383
- break;
384
- case 'code':
385
- div.innerHTML = `<pre><code>${content}</code></pre>`;
386
- break;
387
- case 'result':
388
- div.innerHTML = `<div class="code-execution-result">${content}</div>`;
389
- break;
390
- case 'image':
391
- div.innerHTML = `<img src="data:image/png;base64,${content}" />`;
392
- break;
393
- default:
394
- div.innerHTML = marked.parse(content);
395
- }
396
-
397
- return div;
398
  };
399
 
400
- // Envoi de l'image pour résolution
401
  form.addEventListener('submit', async e => {
402
  e.preventDefault();
403
  const file = imageInput.files[0];
404
- if (!file) {
405
- Swal.fire({
406
- icon: 'error',
407
- title: 'Image manquante',
408
- text: 'Veuillez sélectionner une image.'
409
- });
410
- return;
411
  }
412
-
 
413
  startTimer();
414
  loader.classList.remove('hidden');
415
  solutionSection.classList.add('hidden');
 
416
  thoughtsContent.innerHTML = '';
417
- answerContent.innerHTML = '';
418
  thoughtsBuffer = '';
419
- answerBuffer = '';
420
  currentMode = null;
421
- thoughtsBox.classList.add('open');
 
422
 
423
  const formData = new FormData();
424
  formData.append('image', file);
425
-
426
  try {
427
  const response = await fetch(`/${currentEndpoint}`, { method: 'POST', body: formData });
 
 
 
 
 
428
  const reader = response.body.getReader();
429
  const decoder = new TextDecoder();
430
- let buffer = '';
431
-
432
- const processChunk = async chunk => {
433
- buffer += decoder.decode(chunk, { stream: true });
434
- const lines = buffer.split('\n\n');
435
- buffer = lines.pop();
436
-
437
- for (const line of lines) {
438
- if (!line.startsWith('data:')) continue;
439
- const data = JSON.parse(line.slice(5));
440
-
441
- if (data.mode) {
442
- currentMode = data.mode;
443
- loader.classList.add('hidden');
444
- solutionSection.classList.remove('hidden');
445
  }
446
-
447
- if (data.content) {
448
- // Gestion différenciée selon le type de contenu
449
- if (data.type) {
450
- if (currentMode === 'thinking') {
451
- // Pour le mode thinking, on accumule tout en texte
452
- thoughtsBuffer += data.content;
453
- } else if (currentMode === 'answering') {
454
- // Pour le mode answering, on traite selon le type
455
- switch(data.type) {
456
- case 'code':
457
- answerBuffer += "\n```\n" + data.content + "\n```\n";
458
- break;
459
- case 'result':
460
- answerBuffer += "\n> " + data.content.replace(/\n/g, "\n> ") + "\n";
461
- break;
462
- case 'image':
463
- // On traitera les images séparément lors de l'affichage
464
- answerBuffer += `\n![Résultat](data:image/png;base64,${data.content})\n`;
465
- break;
466
- default:
467
- answerBuffer += data.content;
468
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  }
470
- } else {
471
- // Rétrocompatibilité avec l'ancien format
472
- if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
473
- else if (currentMode === 'answering') { answerBuffer += data.content; }
474
- }
475
  }
476
-
477
- if (data.error) {
478
- answerBuffer += `\n**Erreur:** ${data.error}\n`;
479
- }
480
- }
481
-
482
- scheduleUpdate();
 
 
 
 
 
483
  };
484
-
485
- while (true) {
486
- const { done, value } = await reader.read();
487
- if (done) {
488
- if (buffer) {
489
- try {
490
- const data = JSON.parse(buffer.slice(5));
491
- if (data.content) {
492
- if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
493
- else if (currentMode === 'answering') { answerBuffer += data.content; }
494
- }
495
- } catch (e) {
496
- console.error('Erreur lors du parsing du dernier morceau:', e);
497
- }
498
- }
499
- scheduleUpdate();
500
- break;
501
- }
502
- await processChunk(value);
503
- }
504
-
505
- stopTimer();
506
- } catch (error) {
507
- console.error('Erreur:', error);
508
- Swal.fire({
509
- icon: 'error',
510
- title: 'Erreur',
511
- text: 'Une erreur est survenue lors du traitement de votre demande.'
512
  });
 
 
 
 
513
  loader.classList.add('hidden');
 
514
  stopTimer();
515
  }
516
  });
517
 
518
- // Sauvegarde de la solution avec SweetAlert2
 
 
 
519
  saveButton.addEventListener('click', () => {
520
- Swal.fire({
521
- title: 'Sauvegarder la solution',
522
- input: 'text',
523
- inputLabel: 'Nom de la sauvegarde',
524
- inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
525
- showCancelButton: true,
526
- confirmButtonText: 'Sauvegarder',
527
- cancelButtonText: 'Annuler'
528
- }).then((result) => {
529
- if (result.isConfirmed && result.value) {
530
- const saveName = result.value;
531
- const saveData = {
532
- answer: answerContent.innerHTML,
533
- thinking: thoughtsContent.innerHTML,
534
- date: new Date().toLocaleString()
535
- };
536
- let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
537
- savedExercises[saveName] = saveData;
538
- localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
539
  Swal.fire({
540
- icon: 'success',
541
- title: 'Sauvegarde réussie',
542
- text: 'Votre solution a bien été sauvegardée !',
543
- timer: 2000,
544
- showConfirmButton: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  });
546
- }
547
- });
548
  });
549
 
550
- // Chargement des sauvegardes dans le modal
551
  const loadSavedList = () => {
552
- savedList.innerHTML = '';
553
- const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
554
-
555
- if (Object.keys(savedExercises).length === 0) {
556
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
557
  return;
558
  }
559
-
560
- for (const [name, data] of Object.entries(savedExercises)) {
561
  const li = document.createElement('li');
562
- li.className = 'border-b pb-2';
563
- li.innerHTML = `
564
- <div class="flex justify-between items-center">
565
- <button class="text-left text-blue-600 hover:underline" data-save="${name}">
566
- ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
567
- </button>
568
- <button class="text-red-500 hover:text-red-700" data-delete="${name}">
569
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
570
- <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" />
571
- </svg>
572
- </button>
573
- </div>
 
 
 
 
 
 
 
 
 
 
574
  `;
 
 
 
575
  savedList.appendChild(li);
576
- }
577
  };
578
 
579
- // Gestion des clics sur les sauvegardes
580
  savedList.addEventListener('click', (e) => {
581
- // Chargement d'une sauvegarde
582
- if (e.target && e.target.dataset.save) {
583
- const saveName = e.target.dataset.save;
584
- const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
585
- const data = savedExercises[saveName];
 
 
586
  if (data) {
587
  form.classList.add('hidden');
588
  loader.classList.add('hidden');
589
  solutionSection.classList.remove('hidden');
590
- thoughtsContent.innerHTML = data.thinking;
591
- answerContent.innerHTML = data.answer;
592
  savedModal.classList.remove('active');
593
-
594
- // Réappliquer MathJax et highlighting
595
- MathJax.typesetPromise().then(() => {
596
- applyHighlighting();
597
- });
 
 
 
 
 
 
 
 
 
598
  }
599
- }
600
-
601
- // Suppression d'une sauvegarde
602
- if (e.target && (e.target.dataset.delete || e.target.closest('[data-delete]'))) {
603
- const deleteName = e.target.dataset.delete || e.target.closest('[data-delete]').dataset.delete;
604
-
605
  Swal.fire({
606
  title: 'Êtes-vous sûr ?',
607
- text: "Cette sauvegarde sera définitivement supprimée.",
608
  icon: 'warning',
609
  showCancelButton: true,
610
- confirmButtonColor: '#3085d6',
611
- cancelButtonColor: '#d33',
612
  confirmButtonText: 'Oui, supprimer',
613
  cancelButtonText: 'Annuler'
614
  }).then((result) => {
615
  if (result.isConfirmed) {
616
- const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
617
- delete savedExercises[deleteName];
618
- localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
619
-
620
- Swal.fire(
621
- 'Supprimé !',
622
- 'La sauvegarde a été supprimée.',
623
- 'success'
624
- );
625
-
626
- loadSavedList();
627
  }
628
  });
629
  }
@@ -632,19 +821,39 @@
632
  // Ouverture / fermeture du modal de sauvegardes
633
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
634
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
 
 
 
 
 
 
 
635
 
636
- // Bouton présent uniquement dans le modal pour lancer un nouvel exercice
637
- newExercise.addEventListener('click', () => {
638
- form.reset();
639
  form.classList.remove('hidden');
640
  solutionSection.classList.add('hidden');
641
- imagePreview.classList.add('hidden');
 
 
 
642
  thoughtsContent.innerHTML = '';
643
  answerContent.innerHTML = '';
644
  thoughtsBuffer = '';
645
- answerBuffer = '';
646
- savedModal.classList.remove('active');
647
- });
 
 
 
 
 
 
 
 
 
 
648
  });
649
  </script>
650
  </body>
 
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
 
 
25
  },
26
  options: {
27
  enableMenu: false,
28
+ messageStyle: 'none',
29
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'] // Important pour Highlight.js
30
  },
31
  startup: {
32
  pageReady: () => { window.mathJaxReady = true; }
 
39
  <style>
40
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
41
  body { font-family: 'Space Grotesk', sans-serif; }
42
+
43
  .uploadArea {
44
  background: #f3f4f6;
45
  border: 2px dashed #d1d5db;
46
  transition: border-color 0.2s ease;
47
  }
48
  .uploadArea:hover { border-color: #3b82f6; }
49
+
50
  .blue-button { background: #3b82f6; transition: background-color 0.2s ease; }
51
  .blue-button:hover { background: #2563eb; }
52
+
53
  .loader {
54
  width: 48px;
55
  height: 48px;
 
60
  animation: rotation 1s linear infinite;
61
  }
62
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
63
+
64
  .thought-box {
65
  transition: max-height 0.3s ease-out;
66
  max-height: 0;
67
  overflow: hidden;
68
  }
69
  .thought-box.open { max-height: 500px; }
70
+
71
  #thoughtsContent, #answerContent {
72
+ max-height: 60vh; /* Limite la hauteur pour éviter un scroll trop long */
73
  overflow-y: auto;
74
  scroll-behavior: smooth;
75
+ white-space: normal; /* Permettre le retour à la ligne normal */
76
+ word-wrap: break-word; /* S'assurer que les longs mots/urls ne dépassent pas */
77
+ padding: 1rem; /* Ajouter un peu de padding interne */
78
+ background-color: #fff; /* Fond blanc pour la zone de réponse */
79
+ border: 1px solid #e2e8f0; /* Bordure légère */
80
+ border-radius: 0.375rem; /* Coins arrondis */
81
+ }
82
+
83
+ #thoughtsContent {
84
+ background-color: #f9fafb; /* Fond légèrement différent pour les pensées */
85
  }
86
+
87
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
88
+
89
  .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
90
+
91
  table {
92
  border-collapse: collapse;
93
  width: 100%;
94
  margin-bottom: 1rem;
95
+ border: 1px solid #d1d5db;
96
  }
97
  th, td {
98
  border: 1px solid #d1d5db;
 
100
  text-align: left;
101
  }
102
  th { background-color: #f3f4f6; font-weight: 600; }
103
+ .table-responsive { overflow-x: auto; margin: 1rem 0; } /* Ajouter un margin */
104
+
 
105
  #saveButton {
106
+ background: #10b981; /* Vert pour sauvegarder */
107
  color: white;
108
  padding: 0.5rem 1rem;
109
  border-radius: 0.375rem;
110
  transition: background-color 0.2s ease;
111
  }
112
+ #saveButton:hover { background: #059669; }
113
+
 
114
  #savedModal {
115
  display: none;
116
  position: fixed;
 
118
  background: rgba(0,0,0,0.5);
119
  z-index: 50;
120
  }
121
+ #savedModal.active { display: flex; align-items: center; justify-content: center;} /* Centrer le modal */
122
  #savedModalContent {
123
  background: #fff;
124
+ width: 90%;
125
+ max-width: 800px; /* Limiter la largeur max */
126
+ max-height: 90vh; /* Limiter la hauteur max */
127
  overflow-y: auto;
128
+ border-radius: 0.5rem; /* Coins arrondis pour le modal */
129
+ display: flex;
130
+ flex-direction: column;
131
  }
132
+ #savedModalContent header {
133
+ padding: 1rem 1.5rem;
134
+ }
135
+ #savedListContainer {
136
+ padding: 0 1.5rem 1rem; /* Padding pour la liste */
137
+ flex-grow: 1; /* Permet à la liste de prendre l'espace */
138
+ overflow-y: auto; /* Scroll interne si besoin */
139
+ }
140
+ #savedModalContent .modal-footer {
141
+ padding: 1rem 1.5rem;
142
+ border-top: 1px solid #e2e8f0;
143
+ text-align: right;
144
+ }
145
 
146
  /* Styles spécifiques pour le code et son exécution */
147
  pre {
 
149
  border: 1px solid #e2e8f0;
150
  border-radius: 0.375rem;
151
  padding: 1rem;
152
+ margin: 1rem 0; /* Espace autour des blocs de code */
153
  overflow-x: auto;
154
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
155
+ white-space: pre; /* Conserver les espaces et retours ligne du code */
156
  }
157
+
158
  code {
159
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
160
+ background-color: rgba(209, 213, 219, 0.3); /* Léger fond pour code inline */
161
+ padding: 0.1em 0.3em;
162
+ border-radius: 0.25rem;
163
  }
164
+
165
+ pre code {
166
+ background-color: transparent; /* Pas de fond spécial dans pre */
167
+ padding: 0;
168
+ border-radius: 0;
169
+ }
170
+
171
  .code-execution-result {
172
+ background-color: #f0fff4; /* Fond vert pâle pour résultat */
173
  border-left: 4px solid #48bb78;
174
  padding: 1rem;
175
+ margin: 1rem 0; /* Espace autour */
176
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
177
+ white-space: pre-wrap; /* Permettre retour à la ligne */
178
+ overflow-x: auto; /* Scroll horizontal si nécessaire */
179
  }
180
+
181
  /* Styles pour les types de contenu spécifiques */
182
+ .content-text p:last-child { margin-bottom: 0; } /* Éviter double marge */
183
+ .content-code {}
184
+ .content-result {}
 
 
 
 
 
185
  .content-image { margin: 1rem 0; text-align: center; }
186
+ .content-image img {
187
+ max-width: 100%;
188
+ height: auto; /* Maintenir proportions */
189
+ display: inline-block; /* Pour que text-align fonctionne */
190
+ border: 1px solid #d1d5db; /* Légère bordure autour de l'image */
191
+ border-radius: 0.25rem;
192
+ }
193
 
194
  /* Onglets pour choisir le modèle */
195
  .tabs {
 
197
  border-bottom: 1px solid #e2e8f0;
198
  margin-bottom: 1rem;
199
  }
200
+
201
  .tab {
202
  padding: 0.5rem 1rem;
203
  cursor: pointer;
204
  border-bottom: 2px solid transparent;
205
+ color: #6b7280; /* Gris par défaut */
206
+ transition: color 0.2s ease, border-color 0.2s ease;
207
+ }
208
+ .tab:hover {
209
+ color: #3b82f6;
210
  }
211
+
212
  .tab.active {
213
  border-bottom: 2px solid #3b82f6;
214
  color: #3b82f6;
215
  font-weight: 600;
216
  }
217
+
218
+ .delete-button {
219
+ color: #ef4444; /* Rouge pour supprimer */
220
+ background: none;
221
+ border: none;
222
+ padding: 0.25rem;
223
+ cursor: pointer;
224
+ transition: color 0.2s ease;
225
+ }
226
+ .delete-button:hover {
227
+ color: #dc2626;
228
+ }
229
+ .delete-button svg {
230
+ display: inline-block; /* Empêche les problèmes de layout */
231
+ }
232
  </style>
233
  </head>
234
+ <body class="bg-gray-100 p-4 min-h-screen flex flex-col">
235
+ <div class="max-w-4xl mx-auto w-full bg-white shadow-md rounded-lg p-6 flex-grow flex flex-col">
236
+ <header class="text-center mb-8">
237
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
238
  <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
239
  <div class="mt-4 flex justify-end">
240
+ <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">
241
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1" viewBox="0 0 20 20" fill="currentColor">
242
+ <path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-3.03L5 18V4z" />
243
+ </svg>
244
+ Sauvegardes
245
+ </button>
246
  </div>
247
  </header>
248
 
249
+ <main id="mainContent" class="flex-grow flex flex-col">
250
+ <form id="problemForm" class="space-y-6 flex-grow flex flex-col" novalidate>
251
  <!-- Onglets pour choisir le modèle -->
252
  <div class="tabs">
253
  <div class="tab active" data-endpoint="solve">Gemini Pro</div>
 
255
  </div>
256
 
257
  <!-- Zone de dépôt / sélection d'image -->
258
+ <div class="uploadArea p-8 text-center relative cursor-pointer flex-grow flex flex-col justify-center items-center" aria-label="Zone de dépôt d'image">
259
  <input type="file" id="imageInput" name="image" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
260
  <div class="space-y-3">
261
+ <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center text-blue-400">
262
+ <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
263
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
264
  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" />
265
  </svg>
 
267
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
268
  <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
269
  </div>
270
+ <!-- Aperçu de l'image -->
271
+ <div id="imagePreview" class="hidden mt-4">
272
+ <img id="previewImage" class="preview-image mx-auto border border-gray-300 rounded" alt="Prévisualisation de l'image">
273
+ </div>
274
  </div>
275
+
276
+ <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg flex items-center justify-center">
277
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
278
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
279
+ </svg>
280
  Résoudre le problème
281
  </button>
282
  </form>
 
288
  </div>
289
 
290
  <!-- Zone d'affichage de la solution -->
291
+ <section id="solution" class="hidden mt-8 space-y-6 flex-grow flex flex-col">
292
+
293
+ <!-- Zone Pensées -->
294
+ <div class="border rounded-lg overflow-hidden shadow-sm">
295
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-50 border-b">
296
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
297
+ <div class="flex items-center">
298
+ <span id="timestamp" class="timestamp mr-2"></span>
299
+ <svg id="toggleIcon" class="w-5 h-5 text-gray-500 transform transition-transform duration-200" viewBox="0 0 20 20" fill="currentColor">
300
+ <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
301
+ </svg>
302
+ </div>
303
  </button>
304
+ <div id="thoughtsBox" class="thought-box bg-gray-50">
305
+ <div id="thoughtsContent" class="text-gray-600 text-sm"></div> <!-- #thoughtsContent est DANS #thoughtsBox -->
306
  </div>
307
  </div>
308
+
309
+ <!-- Zone Solution -->
310
+ <div class="border rounded-lg shadow-sm flex-grow flex flex-col">
311
+ <div class="flex justify-between items-center p-3 border-b bg-white">
312
+ <h3 class="text-xl font-bold text-gray-800">Solution</h3>
313
+ <button id="saveButton" title="Sauvegarder cette solution">
314
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1" viewBox="0 0 20 20" fill="currentColor">
315
+ <path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-3.03L5 18V4z" />
316
+ </svg>
317
+ Sauvegarder
318
+ </button>
319
  </div>
320
+ <div id="answerContent" class="text-gray-700 flex-grow bg-white rounded-b-lg"></div> <!-- #answerContent est DANS la div Solution -->
321
  </div>
322
+
323
  </section>
324
  </main>
325
  </div>
326
 
327
  <!-- Modal plein écran pour les sauvegardes -->
328
  <div id="savedModal">
329
+ <div id="savedModalContent" class="p-0"> <!-- Padding géré par les sections internes -->
330
+ <header class="flex justify-between items-center border-b pb-4 p-4">
331
+ <h2 class="text-2xl font-bold text-gray-800">Sauvegardes</h2>
332
+ <button id="closeSaved" class="text-3xl text-gray-500 hover:text-gray-700">×</button>
333
  </header>
334
+ <div id="savedListContainer" class="mt-4 flex-grow overflow-y-auto px-4">
335
  <ul id="savedList" class="space-y-4">
336
  <!-- Liste des sauvegardes insérée dynamiquement -->
337
+ <li class="text-gray-500 text-center py-8">Chargement...</li>
338
  </ul>
339
  </div>
340
+ <div class="modal-footer mt-6 p-4 bg-gray-50 border-t">
341
+ <button id="newExercise" class="blue-button px-4 py-2 text-white font-medium rounded-lg">
342
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1" viewBox="0 0 20 20" fill="currentColor">
343
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
344
+ </svg>
345
+ Nouvel Exercice
346
  </button>
347
  </div>
348
  </div>
 
350
 
351
  <script>
352
  document.addEventListener('DOMContentLoaded', () => {
353
+ // --- Récupération des éléments ---
354
  const form = document.getElementById('problemForm');
355
  const imageInput = document.getElementById('imageInput');
356
  const loader = document.getElementById('loader');
 
359
  const answerContent = document.getElementById('answerContent');
360
  const thoughtsToggle = document.getElementById('thoughtsToggle');
361
  const thoughtsBox = document.getElementById('thoughtsBox');
362
+ const toggleIcon = document.getElementById('toggleIcon');
363
  const imagePreview = document.getElementById('imagePreview');
364
  const previewImage = document.getElementById('previewImage');
365
  const timestamp = document.getElementById('timestamp');
 
371
  const newExercise = document.getElementById('newExercise');
372
  const mainContent = document.getElementById('mainContent');
373
  const tabs = document.querySelectorAll('.tab');
374
+ const dropZone = document.querySelector('.uploadArea'); // Récupérer la drop zone
375
 
376
+ // --- États et Variables ---
377
  let startTime = null;
378
  let timerInterval = null;
379
+ let thoughtsBuffer = ''; // Garder un buffer pour les pensées
 
380
  let currentMode = null;
 
381
  let currentEndpoint = 'solve'; // Par défaut: 'solve' pour Gemini Pro
382
+ let renderTimeout; // Pour le debouncing
383
+
384
+ // --- Configuration Marked ---
385
+ marked.setOptions({
386
+ gfm: true,
387
+ breaks: true,
388
+ highlight: function (code, lang) {
389
+ const language = hljs.getLanguage(lang) ? lang : 'plaintext';
390
+ try {
391
+ // Structure attendue par marked avec hljs
392
+ return hljs.highlight(code, { language, ignoreIllegals: true }).value;
393
+ } catch (error) {
394
+ console.error("Highlighting error:", error);
395
+ // Retour en plain text en cas d'erreur
396
+ return hljs.highlight(code, { language: 'plaintext', ignoreIllegals: true }).value;
397
+ }
398
+ }
399
+ });
400
 
401
+ // --- Gestion des Onglets Modèle ---
402
  tabs.forEach(tab => {
403
  tab.addEventListener('click', () => {
404
  tabs.forEach(t => t.classList.remove('active'));
 
407
  });
408
  });
409
 
410
+ // --- Gestion du Timer ---
411
  const updateTimestamp = () => {
412
  if (startTime) {
413
  const seconds = Math.floor((Date.now() - startTime) / 1000);
 
415
  }
416
  };
417
  const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
418
+ const stopTimer = () => { clearInterval(timerInterval); updateTimestamp(); /* Met à jour une dernière fois */ };
419
 
420
+ // --- Gestion Fichiers & Prévisualisation ---
421
  const handleFileSelect = file => {
422
+ if (!file || !file.type.startsWith('image/')) {
423
+ Swal.fire('Fichier invalide', 'Veuillez sélectionner un fichier image.', 'warning');
424
+ imagePreview.classList.add('hidden'); // Cacher si invalide
425
+ imageInput.value = ''; // Réinitialiser l'input file
426
+ return null; // Indiquer qu'aucun fichier valide n'a été sélectionné
427
+ }
428
  const reader = new FileReader();
429
  reader.onload = e => {
430
  previewImage.src = e.target.result;
431
  imagePreview.classList.remove('hidden');
432
  };
433
  reader.readAsDataURL(file);
434
+ return file; // Retourner le fichier valide
435
  };
436
 
437
+ imageInput.addEventListener('change', e => {
438
+ if (e.target.files.length > 0) {
439
+ handleFileSelect(e.target.files[0]);
440
+ } else {
441
+ imagePreview.classList.add('hidden');
442
+ }
443
+ });
 
 
 
 
 
 
 
 
444
 
445
+ // --- Gestion Glisser-Déposer ---
446
+ dropZone.addEventListener('click', () => imageInput.click()); // Rendre toute la zone cliquable
447
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400', 'bg-blue-50'); });
448
+ dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400', 'bg-blue-50'); });
449
+ dropZone.addEventListener('drop', e => {
450
+ e.preventDefault();
451
+ dropZone.classList.remove('border-blue-400', 'bg-blue-50');
452
+ if (e.dataTransfer.files.length > 0) {
453
+ const file = handleFileSelect(e.dataTransfer.files[0]);
454
+ if (file) {
455
+ // Mettre à jour l'input file pour que le formulaire le trouve
456
+ const dataTransfer = new DataTransfer();
457
+ dataTransfer.items.add(file);
458
+ imageInput.files = dataTransfer.files;
459
+ }
460
+ }
461
+ });
462
 
463
+ // --- Affichage/Masquage Pensées ---
464
+ thoughtsToggle.addEventListener('click', () => {
465
+ thoughtsBox.classList.toggle('open');
466
+ toggleIcon.classList.toggle('rotate-180'); // Pivoter l'icône chevron
467
+ });
 
468
 
469
+ // --- Debouncer pour Rendu MathJax/Highlight ---
470
+ const debouncedRender = () => {
471
+ clearTimeout(renderTimeout);
472
+ renderTimeout = setTimeout(async () => {
473
+ // Highlightjs d'abord, car MathJax peut interférer avec les classes hljs
474
+ document.querySelectorAll('#answerContent pre code:not(.hljs)').forEach((block) => {
475
+ try {
476
+ hljs.highlightElement(block);
477
+ } catch (e) { console.error("Highlighting error on block:", e, block); }
478
+ });
479
 
480
+ // Puis MathJax
481
+ if (window.mathJaxReady && typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
482
+ console.log("Typesetting MathJax...");
483
+ await MathJax.typesetPromise([answerContent]).catch(err => console.error('MathJax Typesetting Error:', err));
484
+ console.log("MathJax Typesetting Done.");
485
+ } else {
486
+ console.log("MathJax not ready or typesetPromise not available");
487
+ }
488
+ // S'assurer que le scroll est bien en bas après le rendu
489
+ answerContent.scrollTop = answerContent.scrollHeight;
 
 
490
 
491
+ }, 300); // Délai de 300ms
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  };
493
 
494
+ // --- Soumission du Formulaire et Traitement SSE ---
495
  form.addEventListener('submit', async e => {
496
  e.preventDefault();
497
  const file = imageInput.files[0];
498
+ if (!file) {
499
+ Swal.fire({ icon: 'error', title: 'Image manquante', text: 'Veuillez sélectionner une image valide.' });
500
+ return;
 
 
 
 
501
  }
502
+
503
+ // Réinitialisation de l'interface
504
  startTimer();
505
  loader.classList.remove('hidden');
506
  solutionSection.classList.add('hidden');
507
+ form.classList.add('hidden'); // Cacher le formulaire pendant le traitement
508
  thoughtsContent.innerHTML = '';
509
+ answerContent.innerHTML = ''; // Vider le contenu précédent
510
  thoughtsBuffer = '';
 
511
  currentMode = null;
512
+ thoughtsBox.classList.remove('open'); // Fermer les pensées par défaut
513
+ toggleIcon.classList.remove('rotate-180');
514
 
515
  const formData = new FormData();
516
  formData.append('image', file);
517
+
518
  try {
519
  const response = await fetch(`/${currentEndpoint}`, { method: 'POST', body: formData });
520
+
521
+ if (!response.ok || !response.body) {
522
+ throw new Error(`Erreur serveur: ${response.status} ${response.statusText}`);
523
+ }
524
+
525
  const reader = response.body.getReader();
526
  const decoder = new TextDecoder();
527
+ buffer = ''; // Buffer pour les données SSE incomplètes
528
+
529
+ // Fonction récursive pour lire le stream
530
+ const processText = async ({ done, value }) => {
531
+ if (done) {
532
+ console.log("Stream terminé.");
533
+ stopTimer();
534
+ loader.classList.add('hidden');
535
+ solutionSection.classList.remove('hidden'); // Afficher la section solution
536
+ // Appel final de rendu pour s'assurer que tout est traité
537
+ debouncedRender();
538
+ return;
 
 
 
539
  }
540
+
541
+ buffer += decoder.decode(value, { stream: true });
542
+ const lines = buffer.split('\n\n');
543
+ buffer = lines.pop(); // Garder la dernière ligne potentiellement incomplète
544
+
545
+ for (const line of lines) {
546
+ if (!line.startsWith('data:')) continue;
547
+
548
+ try {
549
+ const data = JSON.parse(line.slice(5));
550
+
551
+ if (data.mode) {
552
+ currentMode = data.mode;
553
+ if (currentMode === 'thinking' && !thoughtsBox.classList.contains('open')) {
554
+ thoughtsBox.classList.add('open'); // Ouvrir automatiquement si pensée détectée
555
+ toggleIcon.classList.add('rotate-180');
556
+ }
557
+ // Afficher la section solution dès qu'on commence à répondre
558
+ if (currentMode === 'answering' && solutionSection.classList.contains('hidden')) {
559
+ loader.classList.add('hidden');
560
+ solutionSection.classList.remove('hidden');
561
+ }
562
+ }
563
+
564
+ if (data.error) {
565
+ const errorEl = document.createElement('div');
566
+ errorEl.className = 'my-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded';
567
+ errorEl.textContent = `Erreur Stream: ${data.error}`;
568
+ answerContent.appendChild(errorEl);
569
+ answerContent.scrollTop = answerContent.scrollHeight;
570
+ continue; // Passer au prochain message
571
+ }
572
+
573
+ if (data.content) {
574
+ if (currentMode === 'thinking') {
575
+ // Mettre à jour le buffer et l'affichage des pensées
576
+ thoughtsBuffer += data.content;
577
+ // On peut décommenter la ligne suivante si on veut un rendu live des pensées (peut impacter perf)
578
+ // thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
579
+ // Ou juste ajouter le texte brut
580
+ thoughtsContent.textContent += data.content; // Plus simple, moins de parsing
581
+ thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
582
+
583
+ } else if (currentMode === 'answering' && data.type) {
584
+ let el;
585
+ switch (data.type) {
586
+ case 'text':
587
+ el = document.createElement('div');
588
+ el.className = 'content-text';
589
+ // Parse seulement ce bout de texte
590
+ el.innerHTML = marked.parse(data.content);
591
+ // Append intelligemment pour éviter trop de <p> vides si on reçoit juste "$...$"
592
+ if (el.firstChild && el.firstChild.nodeName === 'P' && !el.firstChild.nextSibling && el.firstChild.innerHTML.trim() === data.content.trim()) {
593
+ // Si le contenu parsé est juste le contenu original dans un <p>, on ajoute le contenu brut (MathJax le prendra)
594
+ answerContent.append(document.createTextNode(data.content));
595
+ } else {
596
+ // Sinon, on ajoute le HTML parsé
597
+ answerContent.appendChild(el);
598
+ }
599
+ break;
600
+ case 'code':
601
+ // Utiliser marked pour créer le <pre><code> avec la bonne classe pour hljs
602
+ el = document.createElement('div');
603
+ el.className = 'content-code';
604
+ el.innerHTML = marked.parse('```' + (data.lang || '') + '\n' + data.content + '\n```');
605
+ answerContent.appendChild(el.firstChild); // Ajouter directement le <pre> généré par marked
606
+ break;
607
+ case 'result':
608
+ el = document.createElement('pre'); // Utiliser <pre> pour préserver le formatage
609
+ el.className = 'code-execution-result';
610
+ el.textContent = data.content;
611
+ answerContent.appendChild(el);
612
+ break;
613
+ case 'image':
614
+ el = document.createElement('div');
615
+ el.className = 'content-image'; // Classe CSS pour le style
616
+ el.innerHTML = `<img src="data:image/png;base64,${data.content}" alt="Generated image" class="inline-block max-w-full h-auto border rounded">`; // Styles inline/tailwind
617
+ answerContent.appendChild(el);
618
+ break;
619
+ default:
620
+ console.warn("Type de contenu inconnu reçu:", data.type, data.content);
621
+ // Ajouter comme texte simple en fallback
622
+ el = document.createElement('span');
623
+ el.textContent = data.content;
624
+ answerContent.appendChild(el);
625
+ }
626
+ // Scroller et demander un rendu
627
+ answerContent.scrollTop = answerContent.scrollHeight;
628
+ debouncedRender();
629
+ }
630
+ }
631
+ } catch (e) {
632
+ console.error("Erreur parsing JSON du stream:", e, "Ligne:", line);
633
  }
 
 
 
 
 
634
  }
635
+ // Lire le chunk suivant
636
+ reader.read().then(processText).catch(streamError => {
637
+ console.error("Erreur de lecture du stream:", streamError);
638
+ stopTimer();
639
+ loader.classList.add('hidden');
640
+ // Afficher l'erreur dans la zone de réponse
641
+ const errorEl = document.createElement('div');
642
+ errorEl.className = 'my-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded';
643
+ errorEl.textContent = `Erreur de lecture: ${streamError.message}`;
644
+ answerContent.appendChild(errorEl);
645
+ solutionSection.classList.remove('hidden'); // Afficher la section pour voir l'erreur
646
+ });
647
  };
648
+
649
+ // Démarrer la lecture du stream
650
+ reader.read().then(processText).catch(initialReadError => {
651
+ console.error("Erreur initiale de lecture du stream:", initialReadError);
652
+ stopTimer();
653
+ loader.classList.add('hidden');
654
+ Swal.fire('Erreur Réseau', 'Impossible de démarrer la lecture de la réponse du serveur.', 'error');
655
+ form.classList.remove('hidden'); // Ré-afficher le formulaire si erreur initiale
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  });
657
+
658
+ } catch (error) {
659
+ console.error('Erreur fetch ou setup:', error);
660
+ Swal.fire({ icon: 'error', title: 'Erreur', text: `Une erreur est survenue: ${error.message}` });
661
  loader.classList.add('hidden');
662
+ form.classList.remove('hidden'); // Ré-afficher le formulaire en cas d'erreur
663
  stopTimer();
664
  }
665
  });
666
 
667
+ // --- Gestion Sauvegardes ---
668
+ const SAVED_EXERCISES_KEY = 'savedExercises_mariam_m1';
669
+
670
+ // Sauvegarde
671
  saveButton.addEventListener('click', () => {
672
+ // Finaliser le rendu avant sauvegarde
673
+ if (thoughtsBuffer) {
674
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer); // Assurer que les pensées sont parsées
675
+ }
676
+ // Appeler debouncedRender une dernière fois et attendre un peu
677
+ debouncedRender();
678
+ setTimeout(() => {
 
 
 
 
 
 
 
 
 
 
 
 
679
  Swal.fire({
680
+ title: 'Sauvegarder la solution',
681
+ input: 'text',
682
+ inputLabel: 'Nom de la sauvegarde',
683
+ inputPlaceholder: 'Ex: Exercice Trigonométrie Ch. 5',
684
+ showCancelButton: true,
685
+ confirmButtonText: 'Sauvegarder',
686
+ cancelButtonText: 'Annuler',
687
+ inputValidator: (value) => {
688
+ if (!value) {
689
+ return 'Veuillez entrer un nom !'
690
+ }
691
+ }
692
+ }).then((result) => {
693
+ if (result.isConfirmed && result.value) {
694
+ const saveName = result.value;
695
+ const saveData = {
696
+ name: saveName, // Stocker le nom aussi dans l'objet
697
+ answer: answerContent.innerHTML, // Sauvegarder le HTML rendu
698
+ thinking: thoughtsContent.innerHTML, // Sauvegarder le HTML rendu
699
+ date: new Date().toISOString() // Format ISO pour tri plus facile
700
+ };
701
+ let savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
702
+ // Vérifier si un nom existe déjà (optionnel: proposer remplacement)
703
+ const existingIndex = savedExercises.findIndex(item => item.name === saveName);
704
+ if (existingIndex > -1) {
705
+ // Remplacer l'ancienne sauvegarde
706
+ savedExercises[existingIndex] = saveData;
707
+ } else {
708
+ savedExercises.push(saveData);
709
+ }
710
+ // Trier par date (plus récent en premier)
711
+ savedExercises.sort((a, b) => new Date(b.date) - new Date(a.date));
712
+
713
+ localStorage.setItem(SAVED_EXERCISES_KEY, JSON.stringify(savedExercises));
714
+ Swal.fire({
715
+ icon: 'success',
716
+ title: 'Sauvegarde réussie!',
717
+ timer: 1500,
718
+ showConfirmButton: false
719
+ });
720
+ }
721
  });
722
+ }, 500); // Attendre 500ms que le rendu final soit probablement terminé
 
723
  });
724
 
725
+ // Chargement de la liste
726
  const loadSavedList = () => {
727
+ savedList.innerHTML = ''; // Vider la liste actuelle
728
+ const savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
729
+
730
+ if (savedExercises.length === 0) {
731
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
732
  return;
733
  }
734
+
735
+ savedExercises.forEach(data => {
736
  const li = document.createElement('li');
737
+ li.className = 'border-b pb-3 mb-3 flex justify-between items-center group'; // Ajouter group pour hover sur bouton delete
738
+
739
+ const contentDiv = document.createElement('div');
740
+ contentDiv.className = 'flex-grow mr-4';
741
+
742
+ const loadButton = document.createElement('button');
743
+ loadButton.className = 'text-left text-blue-600 hover:underline focus:outline-none';
744
+ loadButton.dataset.saveName = data.name;
745
+ loadButton.innerHTML = `
746
+ <span class="font-medium">${data.name}</span>
747
+ <span class="block text-gray-500 text-xs">${new Date(data.date).toLocaleString('fr-FR')}</span>
748
+ `;
749
+ contentDiv.appendChild(loadButton);
750
+
751
+ const deleteButton = document.createElement('button');
752
+ deleteButton.className = 'delete-button flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
753
+ deleteButton.dataset.deleteName = data.name;
754
+ deleteButton.title = "Supprimer cette sauvegarde";
755
+ deleteButton.innerHTML = `
756
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
757
+ <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" />
758
+ </svg>
759
  `;
760
+
761
+ li.appendChild(contentDiv);
762
+ li.appendChild(deleteButton);
763
  savedList.appendChild(li);
764
+ });
765
  };
766
 
767
+ // Gestion des clics dans la liste (chargement/suppression)
768
  savedList.addEventListener('click', (e) => {
769
+ const loadBtn = e.target.closest('[data-save-name]');
770
+ const deleteBtn = e.target.closest('[data-delete-name]');
771
+
772
+ if (loadBtn) {
773
+ const saveName = loadBtn.dataset.saveName;
774
+ const savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
775
+ const data = savedExercises.find(item => item.name === saveName);
776
  if (data) {
777
  form.classList.add('hidden');
778
  loader.classList.add('hidden');
779
  solutionSection.classList.remove('hidden');
780
+ thoughtsContent.innerHTML = data.thinking; // Restaurer le HTML
781
+ answerContent.innerHTML = data.answer; // Restaurer le HTML
782
  savedModal.classList.remove('active');
783
+ stopTimer(); // Arrêter le timer si on charge une sauvegarde
784
+ timestamp.textContent = ''; // Effacer le temps écoulé
785
+
786
+ // Forcer le rendu MathJax/Highlight sur le contenu chargé
787
+ setTimeout(() => { // Donner le temps au DOM de se mettre à jour
788
+ if (data.thinking) {
789
+ thoughtsBox.classList.add('open');
790
+ toggleIcon.classList.add('rotate-180');
791
+ } else {
792
+ thoughtsBox.classList.remove('open');
793
+ toggleIcon.classList.remove('rotate-180');
794
+ }
795
+ debouncedRender();
796
+ }, 100);
797
  }
798
+ } else if (deleteBtn) {
799
+ const deleteName = deleteBtn.dataset.deleteName;
 
 
 
 
800
  Swal.fire({
801
  title: 'Êtes-vous sûr ?',
802
+ text: `Supprimer définitivement la sauvegarde "${deleteName}" ?`,
803
  icon: 'warning',
804
  showCancelButton: true,
805
+ confirmButtonColor: '#d33', // Rouge pour confirmer la suppression
806
+ cancelButtonColor: '#3085d6',
807
  confirmButtonText: 'Oui, supprimer',
808
  cancelButtonText: 'Annuler'
809
  }).then((result) => {
810
  if (result.isConfirmed) {
811
+ let savedExercises = JSON.parse(localStorage.getItem(SAVED_EXERCISES_KEY) || '[]');
812
+ savedExercises = savedExercises.filter(item => item.name !== deleteName);
813
+ localStorage.setItem(SAVED_EXERCISES_KEY, JSON.stringify(savedExercises));
814
+ Swal.fire( 'Supprimé !', 'La sauvegarde a été supprimée.', 'success' );
815
+ loadSavedList(); // Recharger la liste
 
 
 
 
 
 
816
  }
817
  });
818
  }
 
821
  // Ouverture / fermeture du modal de sauvegardes
822
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
823
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
824
+ // Fermer le modal si on clique en dehors du contenu
825
+ savedModal.addEventListener('click', (e) => {
826
+ if (e.target === savedModal) {
827
+ savedModal.classList.remove('active');
828
+ }
829
+ });
830
+
831
 
832
+ // Bouton "Nouvel Exercice"
833
+ const resetApp = () => {
834
+ form.reset(); // Réinitialiser les champs du formulaire
835
  form.classList.remove('hidden');
836
  solutionSection.classList.add('hidden');
837
+ loader.classList.add('hidden');
838
+ imagePreview.classList.add('hidden'); // Cacher la prévisualisation
839
+ previewImage.src = ''; // Vider la source de l'image
840
+ imageInput.value = ''; // Important pour pouvoir re-sélectionner le même fichier
841
  thoughtsContent.innerHTML = '';
842
  answerContent.innerHTML = '';
843
  thoughtsBuffer = '';
844
+ currentMode = null;
845
+ stopTimer();
846
+ timestamp.textContent = '';
847
+ thoughtsBox.classList.remove('open');
848
+ toggleIcon.classList.remove('rotate-180');
849
+ savedModal.classList.remove('active'); // Fermer le modal si ouvert
850
+ }
851
+
852
+ newExercise.addEventListener('click', resetApp);
853
+
854
+ // --- Initialisation ---
855
+ resetApp(); // Assurer un état propre au chargement
856
+
857
  });
858
  </script>
859
  </body>