Docfile commited on
Commit
7945c76
·
verified ·
1 Parent(s): 81eb477

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +383 -585
templates/index.html CHANGED
@@ -1,3 +1,5 @@
 
 
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
  <head>
@@ -11,41 +13,29 @@
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.9.0/styles/github.min.css">
15
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
16
- <!-- Include Python language if needed -->
17
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
18
-
19
-
20
- <!-- Marked.js for Markdown parsing -->
21
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
22
 
23
- <!-- MathJax Configuration -->
24
  <script>
25
  window.MathJax = {
26
  tex: {
27
  inlineMath: [['$', '$']],
28
  displayMath: [['$$', '$$']],
29
  processEscapes: true,
30
- packages: {'[+]': ['autoload', 'ams']} // Using autoload package
31
  },
32
  options: {
33
  enableMenu: false,
34
- messageStyle: 'none',
35
- skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'] // Ensure MathJax skips code blocks
36
  },
37
  startup: {
38
- // Use async ready function to ensure MathJax is fully loaded
39
- ready: () => {
40
- console.log('MathJax is ready');
41
- MathJax.startup.defaultReady();
42
- window.mathJaxReady = true; // Set flag when ready
43
- }
44
  }
45
  };
46
  </script>
47
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
48
-
49
 
50
  <style>
51
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
@@ -73,187 +63,131 @@
73
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
74
 
75
  .thought-box {
76
- transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
77
  max-height: 0;
78
  overflow: hidden;
79
- opacity: 0;
80
- border: 1px solid #e5e7eb; /* Add border for clarity */
81
- border-radius: 0.375rem; /* Rounded corners */
82
- margin-top: 0.5rem; /* Spacing */
83
- }
84
- .thought-box.open {
85
- max-height: 500px; /* Or a larger value if needed */
86
- opacity: 1;
87
  }
 
88
 
89
  #thoughtsContent, #answerContent {
90
- max-height: 60vh; /* Use viewport height for better scaling */
91
  overflow-y: auto;
92
  scroll-behavior: smooth;
93
- white-space: pre-wrap; /* Allow text wrapping */
94
- word-wrap: break-word; /* Break long words */
95
- background-color: #f9fafb; /* Slight background tint */
96
- padding: 1rem; /* Padding inside content areas */
97
- }
98
- #answerContent {
99
- background-color: white; /* Keep answer content white */
100
  }
101
 
102
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
103
 
104
- .timestamp { color: #6b7280; font-size: 0.8em; margin-left: 8px; }
105
 
106
  table {
107
  border-collapse: collapse;
108
  width: 100%;
109
  margin-bottom: 1rem;
110
- border: 1px solid #d1d5db;
111
  }
112
  th, td {
113
- border: 1px solid #e5e7eb; /* Lighter border */
114
- padding: 0.75rem; /* More padding */
115
  text-align: left;
116
  }
117
  th { background-color: #f3f4f6; font-weight: 600; }
118
  .table-responsive { overflow-x: auto; }
119
 
 
120
  #saveButton {
121
- background: #10b981; /* Green button */
122
  color: white;
123
  padding: 0.5rem 1rem;
124
  border-radius: 0.375rem;
125
  transition: background-color 0.2s ease;
126
- font-weight: 500;
127
  }
128
- #saveButton:hover { background: #059669; }
129
 
 
130
  #savedModal {
131
  display: none;
132
  position: fixed;
133
  inset: 0;
134
- background: rgba(0,0,0,0.6); /* Darker overlay */
135
  z-index: 50;
136
- overflow-y: auto; /* Allow modal scroll */
137
  }
138
- #savedModal.active { display: flex; align-items: center; justify-content: center; }
139
  #savedModalContent {
140
  background: #fff;
141
- width: 90%;
142
- max-width: 800px; /* Limit max width */
143
- max-height: 90vh; /* Limit max height */
144
- border-radius: 0.5rem;
145
- overflow: hidden; /* Hide inner overflow initially */
146
- display: flex;
147
- flex-direction: column;
148
  }
149
- #savedModalContent header { padding: 1.5rem; border-bottom: 1px solid #e5e7eb; }
150
- #savedModalContent #savedListContainer { padding: 1.5rem; overflow-y: auto; flex-grow: 1; }
151
- #savedModalContent footer { padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; background-color: #f9fafb; }
152
-
153
 
154
- /* Styles specific to code blocks generated by marked.js and highlighted by highlight.js */
155
- #answerContent pre code.hljs { /* Target highlighted code blocks */
156
- display: block;
157
- overflow-x: auto;
158
- padding: 1em;
159
- background: #f3f4f6; /* Background matching Tailwind gray */
160
- color: #1f2937; /* Dark text */
161
  border-radius: 0.375rem;
162
- border: 1px solid #e5e7eb;
163
- }
164
-
165
- /* Style for blockquotes used for code execution results */
166
- #answerContent blockquote {
167
- background-color: #eff6ff; /* Lighter blue background */
168
- border-left: 4px solid #3b82f6; /* Blue left border */
169
- padding: 0.75rem 1rem;
170
  margin: 1rem 0;
171
- color: #1e40af; /* Darker blue text */
172
- white-space: pre-wrap; /* Preserve line breaks and spaces */
173
- word-wrap: break-word; /* Break long lines */
174
- font-style: italic; /* Italicize results */
175
- }
176
- #answerContent blockquote p {
177
- margin: 0; /* Remove default paragraph margins inside blockquote */
178
- }
179
-
180
-
181
- /* Styles for images within the answer */
182
- #answerContent img {
183
- max-width: 100%;
184
- height: auto; /* Maintain aspect ratio */
185
- display: block; /* Prevent extra space below image */
186
- margin: 1rem auto; /* Center images with margin */
187
- border: 1px solid #d1d5db; /* Optional border */
188
- border-radius: 0.25rem; /* Optional rounded corners */
189
  }
190
 
191
- /* Tabs styling */
192
- .tabs {
193
- display: flex;
194
- border-bottom: 1px solid #e2e8f0;
195
- margin-bottom: 1.5rem; /* More space below tabs */
196
  }
197
 
198
- .tab {
199
- padding: 0.75rem 1.5rem; /* More padding */
200
- cursor: pointer;
201
- border-bottom: 3px solid transparent;
202
- color: #4b5563; /* Gray text */
203
- font-weight: 500;
204
- transition: border-color 0.2s ease, color 0.2s ease;
205
  }
206
 
207
- .tab:hover {
208
- color: #1f2937; /* Darker text on hover */
 
 
 
 
 
 
209
  }
 
 
210
 
211
- .tab.active {
212
- border-bottom: 3px solid #3b82f6;
213
- color: #3b82f6; /* Blue text */
214
- font-weight: 600;
215
- }
216
  </style>
217
  </head>
218
- <body class="bg-gray-50 p-4 min-h-screen flex items-center justify-center">
219
- <div class="max-w-4xl mx-auto w-full bg-white p-8 rounded-lg shadow-md">
220
- <header class="p-6 text-center mb-6 border-b border-gray-200">
221
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
222
- <p class="text-gray-600 mt-2">Solution Mathématique/Physique/Chimie Intelligente</p>
223
  <div class="mt-4 flex justify-end">
224
- <button id="openSaved" class="text-sm blue-button px-4 py-2 text-white rounded-md shadow-sm">
225
- Voir Sauvegardes
226
- </button>
227
  </div>
228
  </header>
229
 
230
  <main id="mainContent">
231
  <form id="problemForm" class="space-y-6" novalidate>
232
- <!-- Tabs for model selection -->
233
- <div class="tabs">
234
- <div class="tab active" data-endpoint="solve">Gemini 1.5 Pro</div> <!-- Updated Name -->
235
- <div class="tab" data-endpoint="solved">Gemini 1.5 Flash</div> <!-- Updated Name -->
236
- </div>
237
-
238
- <!-- Image upload area -->
239
- <div class="uploadArea p-8 text-center relative rounded-md" aria-label="Zone de dépôt d'image">
240
  <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">
241
  <div class="space-y-3">
242
- <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center bg-blue-50">
243
- <svg class="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
244
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
245
  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" />
246
  </svg>
247
  </div>
248
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
249
- <p class="text-gray-500 text-sm">ou cliquez pour sélectionner (PNG, JPG, WEBP...)</p>
250
  </div>
251
  </div>
252
- <!-- Image preview -->
253
- <div id="imagePreview" class="hidden text-center mt-4">
254
- <img id="previewImage" class="preview-image mx-auto border border-gray-300 rounded" alt="Prévisualisation de l'image">
255
  </div>
256
- <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg shadow-md hover:shadow-lg transition duration-200">
257
  Résoudre le problème
258
  </button>
259
  </form>
@@ -261,97 +195,55 @@
261
  <!-- Loader -->
262
  <div id="loader" class="hidden mt-8 text-center">
263
  <span class="loader"></span>
264
- <p class="mt-4 text-gray-600 font-medium">Analyse en cours...</p>
265
  </div>
266
 
267
- <!-- Solution display area -->
268
- <section id="solution" class="hidden mt-8 space-y-6">
269
- <!-- Thinking Process Toggle and Content -->
270
- <div class="border rounded-md overflow-hidden">
271
- <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 bg-gray-100 hover:bg-gray-200 transition duration-150">
272
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
273
- <div class="flex items-center">
274
- <span id="timestamp" class="timestamp"></span>
275
- <!-- Simple chevron icon -->
276
- <svg id="toggleIcon" class="w-5 h-5 text-gray-600 ml-2 transform transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
277
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
278
- </svg>
279
- </div>
280
  </button>
281
- <div id="thoughtsBox" class="thought-box"> <!-- Content goes inside the box -->
282
- <div id="thoughtsContent" class="p-4 text-sm text-gray-600"></div>
283
- </div>
284
  </div>
285
-
286
- <!-- Answer Content -->
287
- <div class="border rounded-md">
288
- <div class="flex justify-between items-center p-3 border-b bg-white">
289
- <h3 class="text-xl font-semibold text-gray-800">Solution Détaillée</h3>
290
  <button id="saveButton">Sauvegarder</button>
291
  </div>
292
- <div id="answerContent" class="p-4 text-gray-700 table-responsive">
293
- <!-- Content will be rendered here -->
294
- </div>
295
  </div>
296
  </section>
297
  </main>
298
  </div>
299
 
300
- <!-- Saved Solutions Modal -->
301
  <div id="savedModal">
302
- <div id="savedModalContent">
303
- <header class="flex justify-between items-center">
304
- <h2 class="text-2xl font-bold text-gray-800">Solutions Sauvegardées</h2>
305
- <button id="closeSaved" class="text-3xl text-gray-500 hover:text-gray-700">×</button>
306
  </header>
307
- <div id="savedListContainer">
308
  <ul id="savedList" class="space-y-4">
309
- <!-- Saved items will be listed here -->
310
  </ul>
311
  </div>
312
- <footer class="text-center">
313
  <button id="newExercise" class="blue-button w-full py-3 text-white font-medium rounded-lg">
314
  Résoudre un nouvel exercice
315
  </button>
316
- </footer>
317
  </div>
318
  </div>
319
 
320
  <script>
321
- // Ensure Marked and Highlight.js are loaded before configuring
322
- // Note: hljs might already be available if scripts load synchronously
323
- // but deferring configuration is safer.
324
-
325
- function configureMarked() {
326
- if (typeof marked === 'undefined' || typeof hljs === 'undefined') {
327
- console.log("Waiting for Marked and Highlight.js...");
328
- setTimeout(configureMarked, 100); // Check again shortly
329
- return;
330
- }
331
- console.log("Configuring Marked.js");
332
- marked.setOptions({
333
- gfm: true, // Enable GitHub Flavored Markdown
334
- breaks: true, // Use GFM line breaks (single newline = <br>)
335
- pedantic: false, // Don't be strict about syntax errors
336
- smartLists: true, // Use smarter list behavior
337
- smartypants: false, // Don't auto-correct quotes, dashes, etc.
338
- highlight: function (code, lang) {
339
- const language = hljs.getLanguage(lang) ? lang : 'plaintext';
340
- try {
341
- return hljs.highlight(code, { language: language, ignoreIllegals: true }).value;
342
- } catch (error) {
343
- console.error("Highlighting error:", error);
344
- return hljs.highlight(code, { language: 'plaintext', ignoreIllegals: true }).value; // Fallback
345
- }
346
- }
347
- });
348
- window.markedConfigured = true; // Flag that configuration is done
349
- }
350
-
351
- configureMarked(); // Start configuration attempt
352
-
353
  document.addEventListener('DOMContentLoaded', () => {
354
- // Element retrieval
355
  const form = document.getElementById('problemForm');
356
  const imageInput = document.getElementById('imageInput');
357
  const loader = document.getElementById('loader');
@@ -360,7 +252,6 @@
360
  const answerContent = document.getElementById('answerContent');
361
  const thoughtsToggle = document.getElementById('thoughtsToggle');
362
  const thoughtsBox = document.getElementById('thoughtsBox');
363
- const toggleIcon = document.getElementById('toggleIcon');
364
  const imagePreview = document.getElementById('imagePreview');
365
  const previewImage = document.getElementById('previewImage');
366
  const timestamp = document.getElementById('timestamp');
@@ -370,182 +261,125 @@
370
  const savedModal = document.getElementById('savedModal');
371
  const savedList = document.getElementById('savedList');
372
  const newExercise = document.getElementById('newExercise');
373
- const mainContent = document.getElementById('mainContent'); // Not used currently, but kept
374
- const tabs = document.querySelectorAll('.tab');
375
- const dropZone = document.querySelector('.uploadArea');
376
 
377
- // State variables
378
  let startTime = null;
379
  let timerInterval = null;
380
  let thoughtsBuffer = '';
381
  let answerBuffer = '';
382
- let currentMode = null; // 'thinking', 'answering'
383
  let updateTimeout = null;
384
- let currentEndpoint = 'solve'; // Default endpoint
385
- let eventSource = null; // To hold the EventSource object for potential closing
386
 
387
- // --- Helper Functions ---
388
 
389
- // Update elapsed time
390
  const updateTimestamp = () => {
391
  if (startTime) {
392
  const seconds = Math.floor((Date.now() - startTime) / 1000);
393
- timestamp.textContent = `(${seconds}s)`;
394
- } else {
395
- timestamp.textContent = '';
396
  }
397
  };
398
- const startTimer = () => { startTime = Date.now(); updateTimestamp(); timerInterval = setInterval(updateTimestamp, 1000); };
399
- const stopTimer = () => { clearInterval(timerInterval); updateTimestamp(); /* Keep final time */ }; // Keep showing final time
400
 
401
- // Handle file selection (preview)
402
  const handleFileSelect = file => {
403
- if (!file || !file.type.startsWith('image/')) {
404
- if(file) { // Only show error if a file was actually selected/dropped
405
- Swal.fire('Erreur', 'Veuillez sélectionner un fichier image valide.', 'error');
406
- }
407
- imagePreview.classList.add('hidden');
408
- previewImage.src = '';
409
- imageInput.value = ''; // Clear the input
410
- return false; // Indicate failure
411
- }
412
  const reader = new FileReader();
413
  reader.onload = e => {
414
  previewImage.src = e.target.result;
415
  imagePreview.classList.remove('hidden');
416
  };
417
  reader.readAsDataURL(file);
418
- return true; // Indicate success
419
  };
420
 
421
- // Toggle thoughts box visibility
422
- const toggleThoughts = () => {
423
- thoughtsBox.classList.toggle('open');
424
- toggleIcon.classList.toggle('rotate-180'); // Rotate chevron
425
- }
426
- thoughtsToggle.addEventListener('click', toggleThoughts);
 
 
427
 
428
- // Apply syntax highlighting to code blocks
429
  const applyHighlighting = () => {
430
- // Check if hljs is available
431
- if (typeof hljs !== 'undefined') {
432
- document.querySelectorAll('#answerContent pre code:not(.hljs)').forEach((block) => {
433
- try {
434
- hljs.highlightElement(block);
435
- } catch (error) {
436
- console.error("Error applying highlight.js:", error, block);
437
- }
438
- });
439
- } else {
440
- console.warn("highlight.js not ready for highlighting.");
441
- }
442
  };
443
 
 
 
 
 
 
 
 
 
 
444
 
445
- // Render MathJax and update display (debounced)
446
- const typesetMathJax = async () => {
447
- if (window.mathJaxReady && typeof MathJax !== 'undefined') {
448
- console.log("Typesetting MathJax...");
449
- try {
450
- // Clear previous typesetting state if necessary before re-typesetting
451
- // MathJax.startup.document.reset(); // Use cautiously, might have side effects
452
- // MathJax.startup.document.clear(); // Alternative
453
- await MathJax.typesetPromise([answerContent]); // Target specific container
454
- console.log("MathJax typesetting complete.");
455
- } catch (error) {
456
- console.error('MathJax typesetting error:', error);
457
- }
458
- } else {
459
- console.log("MathJax not ready, deferring typesetting.");
460
- // No need for setTimeout here, updateDisplay handles debouncing
461
- }
462
- };
463
-
464
-
465
- // Update content display (debounced)
466
- const updateDisplay = async () => {
467
- if (!window.markedConfigured) {
468
- console.log("Marked not configured, deferring display update.");
469
- scheduleUpdate(); // Reschedule
470
- return;
471
- }
472
- console.log("Updating display...");
473
- // Use try-catch for parsing as large/complex markdown can sometimes cause issues
474
- try {
475
- thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
476
- answerContent.innerHTML = marked.parse(answerBuffer);
477
-
478
- // Apply Highlighting *after* setting innerHTML from marked
479
- applyHighlighting();
480
-
481
- // Typeset MathJax *after* highlighting (or concurrently if no dependencies)
482
- await typesetMathJax();
483
-
484
- // Scroll to bottom (optional)
485
- answerContent.scrollTop = answerContent.scrollHeight;
486
- thoughtsContent.scrollTop = thoughtsContent.scrollHeight;
487
-
488
- } catch (error) {
489
- console.error("Error parsing Markdown:", error);
490
- // Display error message to user?
491
- answerContent.innerHTML = "<p class='text-red-500'>Erreur lors de l'affichage de la réponse.</p>";
492
- }
493
-
494
- updateTimeout = null; // Clear timeout flag
495
- };
496
-
497
-
498
- // Schedule display update (debounce mechanism)
499
- const scheduleUpdate = () => {
500
- if (!updateTimeout) {
501
- updateTimeout = setTimeout(updateDisplay, 250); // Debounce time (250ms)
502
- }
503
  };
504
 
505
- // --- Event Listeners ---
506
 
507
- // Tab switching
508
- tabs.forEach(tab => {
509
- tab.addEventListener('click', () => {
510
- if (eventSource) { // Prevent switching during generation
511
- Swal.fire('Attention', 'Veuillez attendre la fin de la génération actuelle avant de changer de modèle.', 'warning');
512
- return;
513
- }
514
- tabs.forEach(t => t.classList.remove('active'));
515
- tab.classList.add('active');
516
- currentEndpoint = tab.dataset.endpoint;
517
- console.log("Switched to endpoint:", currentEndpoint);
518
- });
519
  });
520
 
521
- // File input change
522
- imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
523
-
524
- // Drag and Drop
525
- dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400', 'bg-blue-100'); });
526
- dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400', 'bg-blue-100'); });
527
- dropZone.addEventListener('drop', e => {
528
- e.preventDefault();
529
- dropZone.classList.remove('border-blue-400', 'bg-blue-100');
530
- if (e.dataTransfer.files.length > 0) {
531
- // Pass the file to the input and the handler
532
- imageInput.files = e.dataTransfer.files;
533
- handleFileSelect(e.dataTransfer.files[0]);
 
 
 
 
 
 
 
534
  }
535
- });
536
 
537
- // Form submission (main logic)
 
 
 
538
  form.addEventListener('submit', async e => {
539
  e.preventDefault();
540
  const file = imageInput.files[0];
541
  if (!file) {
542
- Swal.fire('Image manquante', 'Veuillez sélectionner ou déposer une image.', 'warning');
 
 
 
 
543
  return;
544
  }
545
- // Ensure the preview is updated if a file was selected but not dropped
546
- if (!handleFileSelect(file)) return; // Re-validate just in case
547
 
548
- // --- Reset UI ---
549
  startTimer();
550
  loader.classList.remove('hidden');
551
  solutionSection.classList.add('hidden');
@@ -553,238 +387,193 @@
553
  answerContent.innerHTML = '';
554
  thoughtsBuffer = '';
555
  answerBuffer = '';
556
- currentMode = null;
557
- if (!thoughtsBox.classList.contains('open')) { // Open thoughts box automatically
558
- toggleThoughts();
559
- }
560
- if (eventSource) { // Close previous connection if any
561
- eventSource.close();
562
- console.log("Previous EventSource closed.");
563
- }
564
 
565
- // --- Prepare and Send Request ---
566
  const formData = new FormData();
567
  formData.append('image', file);
568
 
569
- const url = `/${currentEndpoint}`; // Use the selected endpoint
570
- console.log(`Sending request to: ${url}`);
571
-
572
- eventSource = new EventSource(url + `?t=${Date.now()}`, { // Use GET for EventSource traditionally, or adapt backend if POST needed via fetch stream
573
- method: 'POST', // NOTE: EventSource standard doesn't support POST body, this might require a fetch-based stream reader instead
574
- body: formData // This body part is NON-STANDARD for EventSource and likely won't work directly
575
- });
576
-
577
- // --- Use Fetch API for Streaming with POST ---
578
- // Replace the EventSource code above with this Fetch-based approach:
579
- fetch(url, {
580
- method: 'POST',
581
- body: formData
582
- })
583
- .then(response => {
584
- if (!response.ok) {
585
- // Try to read error message from server if available
586
- return response.json().then(errData => {
587
- throw new Error(errData.error || `Erreur serveur: ${response.statusText}`);
588
- }).catch(() => {
589
- // Fallback if response is not JSON or reading fails
590
- throw new Error(`Erreur HTTP ${response.status} - ${response.statusText}`);
591
- });
592
- }
593
- if (!response.body) {
594
- throw new Error("La réponse du serveur n'a pas de corps de flux.");
595
- }
596
- const reader = response.body.getReader();
597
- const decoder = new TextDecoder();
598
- let buffer = '';
599
-
600
- function processStream() {
601
- reader.read().then(({ done, value }) => {
602
- if (done) {
603
- console.log("Stream terminé.");
604
- stopTimer();
605
- if (buffer) { // Process any remaining data in the buffer
606
- try {
607
- const data = JSON.parse(buffer.slice(5)); // Assuming "data: " prefix
608
- processData(data);
609
- } catch (parseError){
610
- console.warn("Could not parse trailing buffer data:", buffer, parseError);
611
- }
612
- }
613
- // Ensure final update is displayed
614
- if(updateTimeout) clearTimeout(updateTimeout);
615
- updateDisplay();
616
- return;
617
- }
618
-
619
- buffer += decoder.decode(value, { stream: true });
620
- // Process buffer line by line (SSE format: data: {...}\n\n)
621
- const lines = buffer.split('\n\n');
622
- buffer = lines.pop() || ''; // Keep the last (potentially incomplete) line
623
-
624
- lines.forEach(line => {
625
- if (line.startsWith('data: ')) {
626
- try {
627
- const jsonData = line.substring(5); // Remove "data: "
628
- const data = JSON.parse(jsonData);
629
- processData(data);
630
- } catch (parseError) {
631
- console.error('Erreur de parsing JSON:', parseError, 'Data received:', line);
632
- }
633
- } else if (line.trim()) {
634
- console.warn("Ligne SSE reçue non reconnue:", line);
635
- }
636
- });
637
 
638
- scheduleUpdate(); // Schedule UI update after processing chunk
639
- processStream(); // Continue reading the stream
 
640
 
641
- }).catch(streamError => {
642
- console.error('Erreur de lecture du flux:', streamError);
643
- Swal.fire('Erreur de Connexion', `Impossible de lire la réponse: ${streamError.message}`, 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  loader.classList.add('hidden');
645
- stopTimer();
646
- });
647
- }
648
- processStream(); // Start processing the stream
649
- })
650
- .catch(error => {
651
- console.error('Erreur Fetch:', error);
652
- Swal.fire('Erreur Réseau', `Impossible de contacter le serveur: ${error.message}`, 'error');
653
- loader.classList.add('hidden');
654
- stopTimer();
655
- });
656
-
657
-
658
- // --- Process Received Data ---
659
- const processData = (data) => {
660
- // console.log("Received data:", data); // Debug log
661
- if (data.mode) {
662
- currentMode = data.mode;
663
- console.log("Mode changed to:", currentMode);
664
- if (currentMode === 'answering' && loader.classList.contains('hidden') === false) {
665
- loader.classList.add('hidden');
666
- solutionSection.classList.remove('hidden');
667
- }
668
- }
669
-
670
- if (data.content) {
671
- const content = data.content;
672
- const type = data.type;
673
-
674
- if (currentMode === 'thinking') {
675
- // Accumulate all thinking content as simple text (or handle types if needed)
676
- thoughtsBuffer += content;
677
- } else if (currentMode === 'answering') {
678
- // Handle different content types for the main answer
679
- switch(type) {
680
- case 'text':
681
- answerBuffer += content;
682
- break;
683
  case 'code':
684
- // Wrap code in Markdown fences with language hint if possible (defaulting to python)
685
- answerBuffer += `\n\n\`\`\`python\n${content}\n\`\`\`\n\n`;
686
- break;
687
  case 'result':
688
- // Format result as a Markdown blockquote
689
- const formattedResult = content.split('\n').map(line => `> ${line}`).join('\n');
690
- answerBuffer += `\n\n${formattedResult}\n\n`;
691
- break;
692
  case 'image':
693
- // --- IMAGE HANDLING CORRECTION ---
694
- const mimeType = data.mime_type || 'image/png'; // Get MIME type or default
695
- // Use Markdown image syntax with the correct data URI
696
- answerBuffer += `\n\n![Image générée](data:${mimeType};base64,${content})\n\n`;
697
- // --- END IMAGE HANDLING CORRECTION ---
698
- break;
699
- default:
700
- console.warn("Type de contenu inconnu reçu:", type, content);
701
- answerBuffer += content; // Append as text by default
702
  }
 
 
 
 
 
 
 
 
 
 
 
703
  }
704
  }
705
 
706
- if (data.error) {
707
- console.error("Erreur reçue du serveur:", data.error);
708
- // Display error prominently in the answer section
709
- answerBuffer += `\n\n<p class="text-red-600 font-bold">Erreur du serveur: ${data.error}</p>\n\n`;
710
- stopTimer(); // Stop timer on error
711
- // Consider stopping the stream/fetch here if possible
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  }
713
- };
 
714
 
715
- }); // End form submit listener
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
 
717
- // --- Local Storage & Modal Logic ---
718
 
719
- // Save solution
720
  saveButton.addEventListener('click', () => {
721
  Swal.fire({
722
  title: 'Sauvegarder la solution',
723
  input: 'text',
724
  inputLabel: 'Nom de la sauvegarde',
725
- inputPlaceholder: 'Ex: Exercice Intégrales',
726
  showCancelButton: true,
727
  confirmButtonText: 'Sauvegarder',
728
- cancelButtonText: 'Annuler',
729
- inputValidator: (value) => {
730
- if (!value) {
731
- return 'Veuillez entrer un nom !'
732
- }
733
- const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
734
- if (savedExercises[value]) {
735
- return 'Ce nom existe déjà. Choisissez-en un autre.'
736
- }
737
- }
738
  }).then((result) => {
739
  if (result.isConfirmed && result.value) {
740
- const saveName = result.value.trim();
741
  const saveData = {
742
- answer: answerContent.innerHTML, // Save rendered HTML
743
- thinking: thoughtsContent.innerHTML, // Save rendered HTML
744
- date: new Date().toLocaleString('fr-FR') // Use French locale for date
745
  };
746
  let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
747
  savedExercises[saveName] = saveData;
748
- try {
749
- localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
750
- Swal.fire({ icon: 'success', title: 'Sauvegardé !', text: `Solution enregistrée sous "${saveName}".`, timer: 2000, showConfirmButton: false });
751
- } catch (e) {
752
- console.error("Error saving to localStorage:", e);
753
- // Handle potential storage limits
754
- Swal.fire('Erreur', 'Impossible de sauvegarder. Le stockage local est peut-être plein.', 'error');
755
- }
756
  }
757
  });
758
  });
759
 
760
- // Load saved list into modal
761
  const loadSavedList = () => {
762
- savedList.innerHTML = ''; // Clear previous list
763
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
764
- const sortedKeys = Object.keys(savedExercises).sort((a, b) => {
765
- // Sort by date descending (newest first) if possible
766
- try {
767
- return new Date(savedExercises[b].date.split('/').reverse().join('-')) - new Date(savedExercises[a].date.split('/').reverse().join('-'));
768
- } catch { return 0; } // Fallback sort
769
- });
770
 
771
-
772
- if (sortedKeys.length === 0) {
773
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
774
  return;
775
  }
776
 
777
- sortedKeys.forEach(name => {
778
- const data = savedExercises[name];
 
 
 
779
  const li = document.createElement('li');
780
- li.className = 'border-b border-gray-200 pb-3 mb-3'; // Styling for list items
781
  li.innerHTML = `
782
  <div class="flex justify-between items-center">
783
- <button class="text-left text-blue-600 hover:underline focus:outline-none" data-save="${encodeURIComponent(name)}">
784
- <span class="font-medium">${name}</span>
785
- <span class="block text-gray-500 text-xs mt-1">(${data.date})</span>
786
  </button>
787
- <button class="text-red-500 hover:text-red-700 p-1 rounded focus:outline-none focus:ring-2 focus:ring-red-400" data-delete="${encodeURIComponent(name)}" aria-label="Supprimer ${name}">
788
  <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
789
  <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" />
790
  </svg>
@@ -792,107 +581,116 @@
792
  </div>
793
  `;
794
  savedList.appendChild(li);
795
- });
796
  };
797
 
798
- // Handle clicks within the saved list (load or delete)
799
  savedList.addEventListener('click', (e) => {
800
- const loadButton = e.target.closest('[data-save]');
801
- const deleteButton = e.target.closest('[data-delete]');
802
-
803
- // Load saved solution
804
- if (loadButton) {
805
- const saveName = decodeURIComponent(loadButton.dataset.save);
806
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
807
  const data = savedExercises[saveName];
808
  if (data) {
809
- form.classList.add('hidden'); // Hide form
 
810
  loader.classList.add('hidden');
811
- solutionSection.classList.remove('hidden'); // Show solution section
812
- thoughtsContent.innerHTML = data.thinking; // Load saved HTML
813
- answerContent.innerHTML = data.answer; // Load saved HTML
814
-
815
- // Re-apply MathJax and Highlighting to the loaded content
816
- // Need slight delay for elements to be in DOM reliably?
817
- setTimeout(async () => {
818
- applyHighlighting();
819
- await typesetMathJax();
820
- // Ensure thoughts box is open if it has content
821
- if(data.thinking && !thoughtsBox.classList.contains('open')) {
822
- toggleThoughts();
823
- } else if (!data.thinking && thoughtsBox.classList.contains('open')) {
824
- toggleThoughts(); // Close if no thinking content was saved
825
- }
826
-
827
- }, 100); // Small delay
828
-
829
- savedModal.classList.remove('active'); // Close modal
 
 
 
 
 
 
 
 
 
830
  }
831
  }
832
 
833
- // Delete saved solution
834
- if (deleteButton) {
835
- const deleteName = decodeURIComponent(deleteButton.dataset.delete);
 
836
  Swal.fire({
837
  title: 'Êtes-vous sûr ?',
838
- text: `Supprimer la sauvegarde "${deleteName}" ? Cette action est irréversible.`,
839
  icon: 'warning',
840
  showCancelButton: true,
841
- confirmButtonColor: '#d33', // Red for delete
842
- cancelButtonColor: '#3085d6',
843
- confirmButtonText: 'Oui, supprimer !',
844
  cancelButtonText: 'Annuler'
845
  }).then((result) => {
846
  if (result.isConfirmed) {
847
- let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
848
  delete savedExercises[deleteName];
849
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
850
- Swal.fire( 'Supprimé !', `La sauvegarde "${deleteName}" a été supprimée.`, 'success' );
851
- loadSavedList(); // Refresh list in modal
 
 
 
 
 
 
 
 
852
  }
853
  });
854
  }
855
  });
856
 
857
- // Modal open/close
858
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
859
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
860
- // Close modal if clicking outside the content area
861
- savedModal.addEventListener('click', (e) => {
862
- if (e.target === savedModal) { // Check if the click is on the backdrop itself
863
- savedModal.classList.remove('active');
864
- }
865
- });
866
 
867
-
868
- // Reset UI for a new exercise (from modal)
869
  newExercise.addEventListener('click', () => {
870
- form.reset(); // Reset form fields
871
- form.classList.remove('hidden'); // Show form
872
- solutionSection.classList.add('hidden'); // Hide solution
873
- loader.classList.add('hidden'); // Hide loader
874
- imagePreview.classList.add('hidden'); // Hide preview
875
- previewImage.src = '';
 
 
876
  thoughtsContent.innerHTML = '';
877
  answerContent.innerHTML = '';
878
  thoughtsBuffer = '';
879
  answerBuffer = '';
880
- stopTimer(); // Reset timer display
881
- if (thoughtsBox.classList.contains('open')) { // Close thoughts box
882
- toggleThoughts();
883
- }
884
- savedModal.classList.remove('active'); // Close modal
885
- });
886
 
887
- // Initial check: If MathJax is already ready, set the flag
888
- if (typeof MathJax !== 'undefined' && MathJax.startup?.promise) {
889
- MathJax.startup.promise.then(() => {
890
- window.mathJaxReady = true;
891
- console.log("MathJax startup promise resolved on load.");
892
- });
893
- }
894
 
895
- }); // End DOMContentLoaded
 
 
 
 
896
  </script>
897
  </body>
898
  </html>
 
1
+ --- START OF FILE index (65).html ---
2
+
3
  <!DOCTYPE html>
4
  <html lang="fr">
5
  <head>
 
13
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
14
 
15
  <!-- Highlight.js pour la coloration syntaxique -->
16
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
 
 
 
 
 
 
18
 
19
+ <!-- Configuration de MathJax -->
20
  <script>
21
  window.MathJax = {
22
  tex: {
23
  inlineMath: [['$', '$']],
24
  displayMath: [['$$', '$$']],
25
  processEscapes: true,
26
+ packages: {'[+]': ['autoload', 'ams']}
27
  },
28
  options: {
29
  enableMenu: false,
30
+ messageStyle: 'none'
 
31
  },
32
  startup: {
33
+ pageReady: () => { window.mathJaxReady = true; }
 
 
 
 
 
34
  }
35
  };
36
  </script>
37
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script" async></script>
38
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
39
 
40
  <style>
41
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap');
 
63
  @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
64
 
65
  .thought-box {
66
+ transition: max-height 0.3s ease-out;
67
  max-height: 0;
68
  overflow: hidden;
 
 
 
 
 
 
 
 
69
  }
70
+ .thought-box.open { max-height: 500px; }
71
 
72
  #thoughtsContent, #answerContent {
73
+ max-height: 500px;
74
  overflow-y: auto;
75
  scroll-behavior: smooth;
76
+ white-space: pre-wrap;
 
 
 
 
 
 
77
  }
78
 
79
  .preview-image { max-width: 300px; max-height: 300px; object-fit: contain; }
80
 
81
+ .timestamp { color: #3b82f6; font-size: 0.9em; margin-left: 8px; }
82
 
83
  table {
84
  border-collapse: collapse;
85
  width: 100%;
86
  margin-bottom: 1rem;
 
87
  }
88
  th, td {
89
+ border: 1px solid #d1d5db;
90
+ padding: 0.5rem;
91
  text-align: left;
92
  }
93
  th { background-color: #f3f4f6; font-weight: 600; }
94
  .table-responsive { overflow-x: auto; }
95
 
96
+ /* Style pour le bouton Sauvegarder afin de le mettre en évidence */
97
  #saveButton {
98
+ background: #3b82f6;
99
  color: white;
100
  padding: 0.5rem 1rem;
101
  border-radius: 0.375rem;
102
  transition: background-color 0.2s ease;
 
103
  }
104
+ #saveButton:hover { background: #2563eb; }
105
 
106
+ /* Modal plein écran pour les sauvegardes */
107
  #savedModal {
108
  display: none;
109
  position: fixed;
110
  inset: 0;
111
+ background: rgba(0,0,0,0.5);
112
  z-index: 50;
 
113
  }
114
+ #savedModal.active { display: block; }
115
  #savedModalContent {
116
  background: #fff;
117
+ width: 100%;
118
+ height: 100%;
119
+ overflow-y: auto;
 
 
 
 
120
  }
 
 
 
 
121
 
122
+ /* Styles spécifiques pour le code et son exécution */
123
+ pre {
124
+ background-color: #f8f8f8;
125
+ border: 1px solid #e2e8f0;
 
 
 
126
  border-radius: 0.375rem;
127
+ padding: 1rem;
 
 
 
 
 
 
 
128
  margin: 1rem 0;
129
+ overflow-x: auto;
130
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  }
132
 
133
+ code {
134
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
 
 
 
135
  }
136
 
137
+ .code-execution-result {
138
+ background-color: #f0fff4;
139
+ border-left: 4px solid #48bb78;
140
+ padding: 1rem;
141
+ margin: 1rem 0;
142
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
143
+ white-space: pre-wrap;
144
  }
145
 
146
+ /* Styles pour les types de contenu spécifiques */
147
+ .content-text {}
148
+ .content-code { padding: 0; }
149
+ .content-result {
150
+ background-color: #f0fff4;
151
+ border-left: 4px solid #48bb78;
152
+ padding: 1rem;
153
+ margin: 0.5rem 0;
154
  }
155
+ .content-image { margin: 1rem 0; text-align: center; }
156
+ .content-image img { max-width: 100%; }
157
 
 
 
 
 
 
158
  </style>
159
  </head>
160
+ <body class="p-4">
161
+ <div class="max-w-4xl mx-auto">
162
+ <header class="p-6 text-center mb-8">
163
  <h1 class="text-4xl font-bold text-blue-600">Mariam M-1</h1>
164
+ <p class="text-gray-600">Solution Mathématique/Physique/Chimie Intelligente</p>
165
  <div class="mt-4 flex justify-end">
166
+ <button id="openSaved" class="blue-button px-4 py-2 text-white rounded">Sauvegardes</button>
 
 
167
  </div>
168
  </header>
169
 
170
  <main id="mainContent">
171
  <form id="problemForm" class="space-y-6" novalidate>
172
+ <!-- Zone de dépôt / sélection d'image -->
173
+ <div class="uploadArea p-8 text-center relative" aria-label="Zone de dépôt d'image">
 
 
 
 
 
 
174
  <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">
175
  <div class="space-y-3">
176
+ <div class="w-16 h-16 mx-auto border-2 border-blue-400 rounded-full flex items-center justify-center">
177
+ <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
178
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
179
  d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
180
  </svg>
181
  </div>
182
  <p class="text-gray-700 font-medium">Déposez votre image ici</p>
183
+ <p class="text-gray-500 text-sm">ou cliquez pour sélectionner</p>
184
  </div>
185
  </div>
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
+ <button type="submit" class="blue-button w-full py-3 text-white font-medium rounded-lg">
191
  Résoudre le problème
192
  </button>
193
  </form>
 
195
  <!-- Loader -->
196
  <div id="loader" class="hidden mt-8 text-center">
197
  <span class="loader"></span>
198
+ <p class="mt-4 text-gray-600">Analyse en cours...</p>
199
  </div>
200
 
201
+ <!-- Zone d'affichage de la solution -->
202
+ <section id="solution" class="hidden mt-8 space-y-6 relative">
203
+ <div class="border-t pt-4">
204
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-2">
 
205
  <span class="font-medium text-gray-700">Processus de Réflexion</span>
206
+ <span id="timestamp" class="timestamp"></span>
 
 
 
 
 
 
207
  </button>
208
+ <div id="thoughtsBox" class="thought-box">
209
+ <div id="thoughtsContent" class="p-4 text-gray-600"></div>
210
+ </div>
211
  </div>
212
+ <div class="border-t pt-6">
213
+ <div class="flex justify-between items-center">
214
+ <h3 class="text-xl font-bold text-gray-800 mb-4">Solution</h3>
215
+ <!-- Bouton Sauvegarder mis en évidence -->
 
216
  <button id="saveButton">Sauvegarder</button>
217
  </div>
218
+ <div id="answerContent" class="text-gray-700 table-responsive"></div>
 
 
219
  </div>
220
  </section>
221
  </main>
222
  </div>
223
 
224
+ <!-- Modal plein écran pour les sauvegardes -->
225
  <div id="savedModal">
226
+ <div id="savedModalContent" class="p-6">
227
+ <header class="flex justify-between items-center border-b pb-4">
228
+ <h2 class="text-2xl font-bold">Sauvegardes</h2>
229
+ <button id="closeSaved" class="text-3xl text-gray-600">×</button>
230
  </header>
231
+ <div id="savedListContainer" class="mt-4">
232
  <ul id="savedList" class="space-y-4">
233
+ <!-- Liste des sauvegardes insérée dynamiquement -->
234
  </ul>
235
  </div>
236
+ <div class="mt-6">
237
  <button id="newExercise" class="blue-button w-full py-3 text-white font-medium rounded-lg">
238
  Résoudre un nouvel exercice
239
  </button>
240
+ </div>
241
  </div>
242
  </div>
243
 
244
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  document.addEventListener('DOMContentLoaded', () => {
246
+ // Récupération des éléments
247
  const form = document.getElementById('problemForm');
248
  const imageInput = document.getElementById('imageInput');
249
  const loader = document.getElementById('loader');
 
252
  const answerContent = document.getElementById('answerContent');
253
  const thoughtsToggle = document.getElementById('thoughtsToggle');
254
  const thoughtsBox = document.getElementById('thoughtsBox');
 
255
  const imagePreview = document.getElementById('imagePreview');
256
  const previewImage = document.getElementById('previewImage');
257
  const timestamp = document.getElementById('timestamp');
 
261
  const savedModal = document.getElementById('savedModal');
262
  const savedList = document.getElementById('savedList');
263
  const newExercise = document.getElementById('newExercise');
264
+ const mainContent = document.getElementById('mainContent'); // This variable is not used in the original logic, keeping it but it could be removed.
 
 
265
 
 
266
  let startTime = null;
267
  let timerInterval = null;
268
  let thoughtsBuffer = '';
269
  let answerBuffer = '';
270
+ let currentMode = null;
271
  let updateTimeout = null;
272
+ // Removed currentEndpoint variable as it's no longer needed
 
273
 
274
+ // Gestion des onglets pour le choix du modèle (Removed as tabs are removed)
275
 
276
+ // Mise à jour du temps écoulé
277
  const updateTimestamp = () => {
278
  if (startTime) {
279
  const seconds = Math.floor((Date.now() - startTime) / 1000);
280
+ timestamp.textContent = `${seconds}s`;
 
 
281
  }
282
  };
283
+ const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
284
+ const stopTimer = () => { clearInterval(timerInterval); startTime = null; timestamp.textContent = ''; };
285
 
286
+ // Affichage de l'image sélectionnée
287
  const handleFileSelect = file => {
288
+ if (!file) return;
 
 
 
 
 
 
 
 
289
  const reader = new FileReader();
290
  reader.onload = e => {
291
  previewImage.src = e.target.result;
292
  imagePreview.classList.remove('hidden');
293
  };
294
  reader.readAsDataURL(file);
 
295
  };
296
 
297
+ thoughtsToggle.addEventListener('click', () => { thoughtsBox.classList.toggle('open'); });
298
+ imageInput.addEventListener('change', e => handleFileSelect(e.target.files[0]));
299
+
300
+ // Gestion du glisser-déposer
301
+ const dropZone = document.querySelector('.uploadArea');
302
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400'); });
303
+ dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); });
304
+ dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400'); handleFileSelect(e.dataTransfer.files[0]); });
305
 
306
+ // Fonction pour appliquer la coloration syntaxique
307
  const applyHighlighting = () => {
308
+ document.querySelectorAll('pre code').forEach((block) => {
309
+ hljs.highlightBlock(block);
310
+ });
 
 
 
 
 
 
 
 
 
311
  };
312
 
313
+ // Rendu MathJax et mise à jour de l'affichage
314
+ const typesetAnswerIfReady = async () => {
315
+ if (window.mathJaxReady) {
316
+ MathJax.startup.document.elements = [document.getElementById('answerContent')];
317
+ await MathJax.typesetPromise();
318
+ applyHighlighting();
319
+ answerContent.scrollTop = answerContent.scrollHeight;
320
+ } else { setTimeout(typesetAnswerIfReady, 200); }
321
+ };
322
 
323
+ const updateDisplay = async () => {
324
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
325
+ answerContent.innerHTML = marked.parse(answerBuffer);
326
+ await typesetAnswerIfReady();
327
+ updateTimeout = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  };
329
 
330
+ const scheduleUpdate = () => { if (!updateTimeout) updateTimeout = setTimeout(updateDisplay, 200); };
331
 
332
+ marked.setOptions({
333
+ gfm: true,
334
+ breaks: true,
335
+ highlight: function(code, lang) {
336
+ if (lang && hljs.getLanguage(lang)) {
337
+ try {
338
+ return hljs.highlight(code, { language: lang }).value;
339
+ } catch (error) {}
340
+ }
341
+ return code;
342
+ }
 
343
  });
344
 
345
+ // Creation of content elements (This part wasn't directly used in the streaming logic before, keeping it just in case it was intended for future use, but the current streaming logic builds markdown directly)
346
+ const createContentElement = (content, type) => {
347
+ const div = document.createElement('div');
348
+ div.className = `content-${type}`;
349
+
350
+ switch(type) {
351
+ case 'text':
352
+ div.innerHTML = marked.parse(content);
353
+ break;
354
+ case 'code':
355
+ div.innerHTML = `<pre><code>${content}</code></pre>`;
356
+ break;
357
+ case 'result':
358
+ div.innerHTML = `<div class="code-execution-result">${content}</div>`;
359
+ break;
360
+ case 'image':
361
+ div.innerHTML = `<img src="data:image/png;base64,${content}" />`;
362
+ break;
363
+ default:
364
+ div.innerHTML = marked.parse(content);
365
  }
 
366
 
367
+ return div;
368
+ };
369
+
370
+ // Envoi de l'image pour résolution
371
  form.addEventListener('submit', async e => {
372
  e.preventDefault();
373
  const file = imageInput.files[0];
374
  if (!file) {
375
+ Swal.fire({
376
+ icon: 'error',
377
+ title: 'Image manquante',
378
+ text: 'Veuillez sélectionner une image.'
379
+ });
380
  return;
381
  }
 
 
382
 
 
383
  startTimer();
384
  loader.classList.remove('hidden');
385
  solutionSection.classList.add('hidden');
 
387
  answerContent.innerHTML = '';
388
  thoughtsBuffer = '';
389
  answerBuffer = '';
390
+ currentMode = null; // Reset mode
391
+
392
+ // thoughtsBox starts open by default, keep it open during processing
393
+ thoughtsBox.classList.add('open');
 
 
 
 
394
 
 
395
  const formData = new FormData();
396
  formData.append('image', file);
397
 
398
+ try {
399
+ // Hardcode the endpoint to /solved
400
+ const response = await fetch('/solved', { method: 'POST', body: formData });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
+ if (!response.body) {
403
+ throw new Error('Response body is not available (e.g., not a streaming response)');
404
+ }
405
 
406
+ const reader = response.body.getReader();
407
+ const decoder = new TextDecoder();
408
+ let buffer = '';
409
+
410
+ const processChunk = async chunk => {
411
+ buffer += decoder.decode(chunk, { stream: true });
412
+ const lines = buffer.split('\n\n');
413
+ buffer = lines.pop();
414
+
415
+ for (const line of lines) {
416
+ if (!line.startsWith('data:')) {
417
+ console.warn('Skipping non-data line:', line); // Log non-data lines
418
+ continue;
419
+ }
420
+ try {
421
+ const data = JSON.parse(line.slice(5));
422
+
423
+ if (data.mode) {
424
+ currentMode = data.mode;
425
  loader.classList.add('hidden');
426
+ solutionSection.classList.remove('hidden');
427
+ }
428
+
429
+ if (data.content !== undefined) { // Check for undefined to allow empty strings
430
+ // Gestion différenciée selon le type de contenu
431
+ if (currentMode === 'thinking') {
432
+ // For thinking mode, just append text
433
+ thoughtsBuffer += data.content;
434
+ } else if (currentMode === 'answering') {
435
+ // For answering mode, handle different types
436
+ switch(data.type) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  case 'code':
438
+ answerBuffer += "\n```\n" + data.content + "\n```\n";
439
+ break;
 
440
  case 'result':
441
+ // Format results clearly, handling multiple lines
442
+ const formattedResult = data.content.split('\n').map(line => `> ${line}`).join('\n');
443
+ answerBuffer += "\n" + formattedResult + "\n";
444
+ break;
445
  case 'image':
446
+ // Include images as markdown images
447
+ answerBuffer += `\n![Résultat](data:image/png;base64,${data.content})\n`;
448
+ break;
449
+ case 'text': // Explicitly handle text within answering mode
450
+ default: // Default behavior is to append text
451
+ answerBuffer += data.content;
452
+ break;
453
+ }
 
454
  }
455
+ }
456
+
457
+ if (data.error) {
458
+ answerBuffer += `\n**Erreur:** ${data.error}\n`;
459
+ }
460
+
461
+ } catch (e) {
462
+ console.error('Error parsing JSON data:', line.slice(5), e);
463
+ // Optionally append an error message to the output
464
+ if (currentMode === 'thinking') { thoughtsBuffer += `\n[Parsing Error]`; }
465
+ else { answerBuffer += `\n[Parsing Error]`; }
466
  }
467
  }
468
 
469
+ scheduleUpdate(); // Schedule display update after processing chunks
470
+ };
471
+
472
+ // Start processing the stream
473
+ while (true) {
474
+ const { done, value } = await reader.read();
475
+ if (done) {
476
+ // Process any remaining buffer
477
+ if (buffer) {
478
+ try {
479
+ // Attempt to parse the last bit if it looks like a data line
480
+ if (buffer.startsWith('data:')) {
481
+ const data = JSON.parse(buffer.slice(5));
482
+ if (data.content !== undefined) {
483
+ if (currentMode === 'thinking') { thoughtsBuffer += data.content; }
484
+ else if (currentMode === 'answering') { answerBuffer += data.content; }
485
+ }
486
+ } else {
487
+ // Handle cases where the buffer is not a complete data line (e.g., partial line)
488
+ console.warn('Remaining buffer is not a complete data line:', buffer);
489
+ // Decide how to handle partial data - maybe discard or append as raw? Appending as raw might break markdown. Discarding is safer.
490
+ }
491
+ } catch (e) {
492
+ console.error('Error processing final buffer:', e);
493
+ }
494
+ }
495
+ scheduleUpdate(); // Final display update
496
+ break; // Exit loop
497
  }
498
+ await processChunk(value);
499
+ }
500
 
501
+ stopTimer(); // Stop timer when stream is done
502
+ } catch (error) {
503
+ console.error('Erreur de Fetch ou du Stream:', error);
504
+ Swal.fire({
505
+ icon: 'error',
506
+ title: 'Erreur de connexion ou de traitement',
507
+ text: `Une erreur est survenue lors du traitement de votre demande: ${error.message}`
508
+ });
509
+ loader.classList.add('hidden');
510
+ stopTimer(); // Ensure timer stops on error
511
+ solutionSection.classList.remove('hidden'); // Show section maybe with error message
512
+ if(answerBuffer === '') { // If no output yet, put error in answer section
513
+ answerContent.innerHTML = `<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
514
+ } else { // If some output exists, append error
515
+ answerBuffer += `\n\n<div class="text-red-500">Une erreur est survenue: ${error.message}</div>`;
516
+ scheduleUpdate(); // Update display with error
517
+ }
518
+ }
519
+ });
520
 
 
521
 
522
+ // Sauvegarde de la solution avec SweetAlert2
523
  saveButton.addEventListener('click', () => {
524
  Swal.fire({
525
  title: 'Sauvegarder la solution',
526
  input: 'text',
527
  inputLabel: 'Nom de la sauvegarde',
528
+ inputPlaceholder: 'Entrez un nom pour cette sauvegarde',
529
  showCancelButton: true,
530
  confirmButtonText: 'Sauvegarder',
531
+ cancelButtonText: 'Annuler'
 
 
 
 
 
 
 
 
 
532
  }).then((result) => {
533
  if (result.isConfirmed && result.value) {
534
+ const saveName = result.value;
535
  const saveData = {
536
+ answer: answerContent.innerHTML, // Save the rendered HTML
537
+ thinking: thoughtsContent.innerHTML, // Save the rendered HTML
538
+ date: new Date().toLocaleString()
539
  };
540
  let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
541
  savedExercises[saveName] = saveData;
542
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
543
+ Swal.fire({
544
+ icon: 'success',
545
+ title: 'Sauvegarde réussie',
546
+ text: 'Votre solution a bien été sauvegardée !',
547
+ timer: 2000,
548
+ showConfirmButton: false
549
+ });
550
  }
551
  });
552
  });
553
 
554
+ // Chargement des sauvegardes dans le modal
555
  const loadSavedList = () => {
556
+ savedList.innerHTML = '';
557
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
 
 
 
 
 
 
558
 
559
+ if (Object.keys(savedExercises).length === 0) {
 
560
  savedList.innerHTML = '<li class="text-gray-500 text-center py-8">Aucune sauvegarde disponible</li>';
561
  return;
562
  }
563
 
564
+ // Sort by date, newest first (optional but nice)
565
+ const sortedEntries = Object.entries(savedExercises).sort(([,a], [,b]) => new Date(b.date) - new Date(a.date));
566
+
567
+
568
+ for (const [name, data] of sortedEntries) {
569
  const li = document.createElement('li');
570
+ li.className = 'border-b pb-2';
571
  li.innerHTML = `
572
  <div class="flex justify-between items-center">
573
+ <button class="text-left text-blue-600 hover:underline" data-save="${name}">
574
+ ${name} <span class="text-gray-500 text-xs">(${data.date})</span>
 
575
  </button>
576
+ <button class="text-red-500 hover:text-red-700" data-delete="${name}">
577
  <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
578
  <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" />
579
  </svg>
 
581
  </div>
582
  `;
583
  savedList.appendChild(li);
584
+ }
585
  };
586
 
587
+ // Gestion des clics sur les sauvegardes
588
  savedList.addEventListener('click', (e) => {
589
+ // Chargement d'une sauvegarde
590
+ if (e.target && e.target.dataset.save) {
591
+ const saveName = e.target.dataset.save;
 
 
 
592
  const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
593
  const data = savedExercises[saveName];
594
  if (data) {
595
+ // Hide the form and loader, show the solution section
596
+ form.classList.add('hidden');
597
  loader.classList.add('hidden');
598
+ solutionSection.classList.remove('hidden');
599
+
600
+ // Load the saved HTML content
601
+ thoughtsContent.innerHTML = data.thinking;
602
+ answerContent.innerHTML = data.answer;
603
+
604
+ // Close the modal
605
+ savedModal.classList.remove('active');
606
+
607
+ // Re-render MathJax and apply highlighting to the loaded HTML
608
+ // Use MathJax.typesetPromise() on the specific elements
609
+ if (window.mathJaxReady) {
610
+ MathJax.startup.document.elements = [thoughtsContent, answerContent]; // Target both potentially
611
+ MathJax.typesetPromise().then(() => {
612
+ applyHighlighting(); // Apply highlighting after MathJax
613
+ }).catch((err) => console.error("MathJax typesetting failed:", err));
614
+ } else {
615
+ console.warn("MathJax not ready yet, cannot typeset saved content.");
616
+ }
617
+
618
+ // Ensure thoughts box is open when viewing a saved solution
619
+ thoughtsBox.classList.add('open');
620
+
621
+ // Reset buffers and timer as this is static content
622
+ thoughtsBuffer = ''; // Or load from saved data if needed for re-processing
623
+ answerBuffer = ''; // Or load from saved data if needed for re-processing
624
+ stopTimer();
625
+ timestamp.textContent = data.date; // Show saved date as timestamp
626
  }
627
  }
628
 
629
+ // Suppression d'une sauvegarde
630
+ if (e.target && (e.target.dataset.delete || e.target.closest('[data-delete]'))) {
631
+ const deleteName = e.target.dataset.delete || e.target.closest('[data-delete]').dataset.delete;
632
+
633
  Swal.fire({
634
  title: 'Êtes-vous sûr ?',
635
+ text: "Cette sauvegarde sera définitivement supprimée.",
636
  icon: 'warning',
637
  showCancelButton: true,
638
+ confirmButtonColor: '#3085d6',
639
+ cancelButtonColor: '#d33',
640
+ confirmButtonText: 'Oui, supprimer',
641
  cancelButtonText: 'Annuler'
642
  }).then((result) => {
643
  if (result.isConfirmed) {
644
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
645
  delete savedExercises[deleteName];
646
  localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
647
+
648
+ Swal.fire(
649
+ 'Supprimé !',
650
+ 'La sauvegarde a été supprimée.',
651
+ 'success'
652
+ );
653
+
654
+ loadSavedList(); // Refresh the list in the modal
655
+ // If the currently viewed solution is the one being deleted, clear it
656
+ // (Optional, maybe better to just let them view it until they start a new one)
657
  }
658
  });
659
  }
660
  });
661
 
662
+ // Ouverture / fermeture du modal de sauvegardes
663
  openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
664
  closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
 
 
 
 
 
 
665
 
666
+ // Bouton présent uniquement dans le modal pour lancer un nouvel exercice
 
667
  newExercise.addEventListener('click', () => {
668
+ // Reset form and hide solution
669
+ form.reset();
670
+ form.classList.remove('hidden');
671
+ solutionSection.classList.add('hidden');
672
+ imagePreview.classList.add('hidden'); // Hide image preview
673
+ previewImage.src = ''; // Clear image source
674
+
675
+ // Clear content areas and buffers
676
  thoughtsContent.innerHTML = '';
677
  answerContent.innerHTML = '';
678
  thoughtsBuffer = '';
679
  answerBuffer = '';
680
+ currentMode = null; // Reset mode
681
+
682
+ // Stop timer and clear timestamp
683
+ stopTimer();
684
+ timestamp.textContent = '';
 
685
 
686
+ // Ensure thoughts box is collapsed for a new exercise
687
+ thoughtsBox.classList.remove('open');
 
 
 
 
 
688
 
689
+
690
+ // Close the modal
691
+ savedModal.classList.remove('active');
692
+ });
693
+ });
694
  </script>
695
  </body>
696
  </html>