Docfile commited on
Commit
699b387
·
verified ·
1 Parent(s): ca871eb

Create index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +710 -0
templates/index.html ADDED
@@ -0,0 +1,710 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mariam M-1 | Solution Math/Physique/Chimie</title>
7
+
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+
10
+ <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
11
+
12
+ <style>
13
+ @font-face {
14
+ font-family: 'LucideIcons';
15
+ src: url(https://cdn.jsdelivr.net/npm/lucide-static@latest/font/Lucide.ttf) format('truetype');
16
+ }
17
+ .lucide {
18
+ font-family: 'LucideIcons';
19
+ font-size: 1.25rem; /* Adjust size as needed */
20
+ line-height: 1;
21
+ font-style: normal;
22
+ font-weight: normal;
23
+ font-variant: normal;
24
+ text-transform: none;
25
+ word-wrap: normal;
26
+ speak: none;
27
+ -webkit-font-smoothing: antialiased;
28
+ -moz-osx-font-smoothing: grayscale;
29
+ }
30
+ </style>
31
+
32
+ <script>
33
+ window.MathJax = {
34
+ tex: {
35
+ inlineMath: [['$', '$']],
36
+ displayMath: [['$$', '$$']],
37
+ processEscapes: true,
38
+ packages: {'[+]': ['ams', 'mhchem']}
39
+ },
40
+ chtml: {
41
+ matchFontHeight: true,
42
+ displayAlign: 'left',
43
+ scale: 1,
44
+ mtextInheritFont: true,
45
+ merrorInheritFont: true,
46
+ mathmlSpacing: false,
47
+ skipAttributes: {},
48
+ exFactor: .5,
49
+ displayIndent: '0'
50
+ },
51
+ options: {
52
+ enableMenu: false,
53
+ ignoreHtmlClass: 'tex2jax_ignore',
54
+ processHtmlClass: 'tex2jax_process',
55
+ renderActions: { addMenu: [0, '', ''] },
56
+ menuOptions: { settings: { zoom: 'None', renderer: 'CHTML' } }
57
+ },
58
+ startup: {
59
+ ready: () => {
60
+ console.log('MathJax is ready to go!');
61
+ MathJax.startup.defaultReady();
62
+ window.mathJaxReady = true;
63
+ }
64
+ }
65
+ };
66
+ </script>
67
+ <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" id="MathJax-script" async></script>
68
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
69
+
70
+ <style>
71
+ /* Use Inter font */
72
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
73
+ body {
74
+ font-family: 'Inter', sans-serif;
75
+ background-color: #f8fafc; /* Light gray background */
76
+ }
77
+
78
+ /* Improved Upload Area Styling */
79
+ .uploadArea {
80
+ background: #ffffff;
81
+ border: 2px dashed #cbd5e1;
82
+ transition: border-color 0.3s ease, background-color 0.3s ease;
83
+ border-radius: 0.75rem;
84
+ }
85
+ .uploadArea:hover {
86
+ border-color: #3b82f6;
87
+ background-color: #eff6ff;
88
+ }
89
+
90
+ /* Consistent Button Styling */
91
+ .blue-button {
92
+ background-color: #3b82f6;
93
+ color: white;
94
+ padding: 0.75rem 1.5rem;
95
+ border-radius: 0.5rem;
96
+ font-weight: 500;
97
+ transition: background-color 0.2s ease, transform 0.1s ease, opacity 0.2s ease;
98
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
99
+ }
100
+ .blue-button:hover:not(:disabled) {
101
+ background-color: #2563eb;
102
+ transform: translateY(-1px);
103
+ }
104
+ .blue-button:active:not(:disabled) {
105
+ transform: translateY(0px);
106
+ }
107
+ .blue-button:disabled {
108
+ opacity: 0.5;
109
+ cursor: not-allowed;
110
+ }
111
+
112
+ /* Save Button Styling (in header) */
113
+ #saveButton {
114
+ background-color: #10b981; /* Green */
115
+ color: white;
116
+ padding: 0.5rem 1rem; /* Smaller padding for header button */
117
+ border-radius: 0.375rem;
118
+ transition: background-color 0.2s ease, opacity 0.2s ease;
119
+ font-weight: 500;
120
+ font-size: 0.875rem; /* text-sm */
121
+ }
122
+ #saveButton:hover:not(:disabled) {
123
+ background-color: #059669; /* Darker green */
124
+ }
125
+ #saveButton:disabled {
126
+ opacity: 0.4; /* More pronounced disabled state */
127
+ cursor: not-allowed;
128
+ background-color: #6ee7b7; /* Lighter green when disabled */
129
+ }
130
+
131
+
132
+ /* Loader Styling */
133
+ .loader {
134
+ width: 40px;
135
+ height: 40px;
136
+ border: 4px solid #3b82f6;
137
+ border-bottom-color: transparent;
138
+ border-radius: 50%;
139
+ display: inline-block;
140
+ animation: rotation 1s linear infinite;
141
+ }
142
+ @keyframes rotation {
143
+ 0% { transform: rotate(0deg); }
144
+ 100% { transform: rotate(360deg); }
145
+ }
146
+
147
+ /* Thought Box Animation */
148
+ .thought-box {
149
+ transition: max-height 0.5s ease-in-out, opacity 0.5s ease-in-out;
150
+ max-height: 0;
151
+ opacity: 0;
152
+ overflow: hidden;
153
+ border-top: 1px solid #e5e7eb;
154
+ margin-top: 0.5rem;
155
+ }
156
+ .thought-box.open {
157
+ max-height: 600px;
158
+ opacity: 1;
159
+ }
160
+
161
+ /* Content Area Styling */
162
+ #thoughtsContent {
163
+ max-height: 600px;
164
+ overflow-y: auto;
165
+ scroll-behavior: smooth;
166
+ padding: 1rem;
167
+ background-color: #f9fafb;
168
+ border-radius: 0.375rem;
169
+ line-height: 1.6;
170
+ color: #374151;
171
+ word-wrap: break-word;
172
+ font-size: 0.875rem; /* sm */
173
+ }
174
+ @media (min-width: 768px) {
175
+ #thoughtsContent { font-size: 1rem; /* base */ }
176
+ }
177
+
178
+ /* Enhanced Answer Content Styling */
179
+ #answerContent {
180
+ max-height: none;
181
+ overflow-y: visible;
182
+ scroll-behavior: smooth;
183
+ padding: 1rem; /* Base padding */
184
+ background-color: #ffffff;
185
+ color: #1f2937;
186
+ word-wrap: break-word;
187
+ font-size: 1rem; /* text-base */
188
+ line-height: 1.7; /* leading-relaxed */
189
+ }
190
+ @media (min-width: 768px) {
191
+ #answerContent {
192
+ font-size: 1.125rem; /* text-lg */
193
+ padding: 1.5rem; /* More padding on larger screens */
194
+ }
195
+ }
196
+ #answerContent pre {
197
+ background-color: #f3f4f6;
198
+ padding: 1rem;
199
+ border-radius: 0.375rem;
200
+ overflow-x: auto;
201
+ margin-top: 0.5rem;
202
+ margin-bottom: 1rem;
203
+ font-size: 0.9em;
204
+ }
205
+ #answerContent code {
206
+ font-family: 'Courier New', Courier, monospace;
207
+ }
208
+ #answerContent img {
209
+ max-width: 100%;
210
+ height: auto;
211
+ margin-top: 0.75rem;
212
+ margin-bottom: 1rem;
213
+ border-radius: 0.375rem;
214
+ border: 1px solid #e5e7eb;
215
+ }
216
+ #answerContent > *:last-child {
217
+ margin-bottom: 0; /* Remove bottom margin from last element */
218
+ }
219
+
220
+ /* MathJax Container Styling */
221
+ #answerContent mjx-container {
222
+ overflow-x: auto;
223
+ overflow-y: hidden;
224
+ padding-bottom: 8px;
225
+ margin-bottom: 1rem;
226
+ scrollbar-width: thin;
227
+ scrollbar-color: #a0aec0 #edf2f7;
228
+ }
229
+ #answerContent mjx-container::-webkit-scrollbar { height: 6px; }
230
+ #answerContent mjx-container::-webkit-scrollbar-track { background: #edf2f7; border-radius: 10px; }
231
+ #answerContent mjx-container::-webkit-scrollbar-thumb { background-color: #a0aec0; border-radius: 10px; border: 1px solid #edf2f7; }
232
+
233
+ /* Image Preview */
234
+ .preview-image {
235
+ max-width: 100%;
236
+ max-height: 300px;
237
+ object-fit: contain;
238
+ border-radius: 0.5rem;
239
+ margin-top: 1rem;
240
+ }
241
+
242
+ /* Timestamp Styling */
243
+ .timestamp { color: #6b7280; font-size: 0.8rem; margin-left: 8px; }
244
+
245
+ /* Table Styling */
246
+ .table-responsive { overflow-x: auto; width: 100%; margin-bottom: 1rem; }
247
+ table { border-collapse: collapse; width: 100%; min-width: 400px; }
248
+ th, td { border: 1px solid #e5e7eb; padding: 0.75rem; text-align: left; vertical-align: top; }
249
+ th { background-color: #f3f4f6; font-weight: 600; color: #1f2937; }
250
+ tbody tr:nth-child(odd) { background-color: #ffffff; }
251
+ tbody tr:nth-child(even) { background-color: #f9fafb; }
252
+
253
+ /* Removed .save-button-container styles */
254
+
255
+ /* Fullscreen Modal Styling */
256
+ #savedModal { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.6); z-index: 50; opacity: 0; transition: opacity 0.3s ease; }
257
+ #savedModal.active { display: flex; align-items: center; justify-content: center; opacity: 1; }
258
+ #savedModalContent { background: #fff; width: 90%; max-width: 600px; max-height: 90vh; border-radius: 0.75rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; overflow: hidden; }
259
+ #savedModal header { padding: 1rem 1.5rem; border-bottom: 1px solid #e5e7eb; }
260
+ #savedListContainer { padding: 1rem 1.5rem; overflow-y: auto; flex-grow: 1; }
261
+ #savedModal footer { padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; background-color: #f9fafb; }
262
+ #closeSaved { background: none; border: none; font-size: 1.75rem; line-height: 1; color: #6b7280; cursor: pointer; transition: color 0.2s ease; }
263
+ #closeSaved:hover { color: #1f2937; }
264
+ #savedList li button { padding: 0.5rem 0; border-radius: 0.25rem; transition: background-color 0.2s ease; display: block; width: 100%; }
265
+ #savedList li button:hover { background-color: #eff6ff; }
266
+ </style>
267
+ </head>
268
+ <body class="p-4 md:p-6">
269
+ <div class="max-w-4xl mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
270
+ <header class="p-5 md:p-6 border-b border-gray-200 relative">
271
+ <div class="flex flex-col sm:flex-row justify-between items-center">
272
+ <div class="text-center sm:text-left mb-4 sm:mb-0">
273
+ <h1 class="text-3xl md:text-4xl font-bold text-blue-600">Mariam M-1</h1>
274
+ <p class="text-gray-500 mt-1 text-sm sm:text-base">Solution Math / Physique / Chimie Intelligente</p>
275
+ </div>
276
+ <div class="flex items-center space-x-2">
277
+ <button id="saveButton" disabled>Sauvegarder</button>
278
+ <button id="openSaved" class="blue-button px-3 py-1.5 text-sm">
279
+ Sauvegardes
280
+ </button>
281
+ </div>
282
+ </div>
283
+ </header>
284
+
285
+ <main id="mainContent" class="p-5 md:p-8">
286
+ <form id="problemForm" class="space-y-6" novalidate>
287
+ <div class="uploadArea p-6 md:p-8 text-center relative cursor-pointer" aria-label="Zone de dépôt d'image">
288
+ <input type="file" id="imageInput" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" aria-label="Choisir une image">
289
+ <div class="space-y-3 pointer-events-none">
290
+ <div class="w-16 h-16 mx-auto border-2 border-blue-300 rounded-full flex items-center justify-center bg-blue-50">
291
+ <span class="lucide text-blue-500" aria-hidden="true">&#xe92a;</span> </div>
292
+ <p class="text-gray-700 font-medium">Déposez votre image ici</p>
293
+ <p class="text-gray-500 text-sm">Images uniquement (.jpg, .png, .gif, etc.)</p>
294
+ </div>
295
+ </div>
296
+ <div id="imagePreview" class="hidden text-center">
297
+ <img id="previewImage" class="preview-image mx-auto border border-gray-200" alt="Prévisualisation de l'image">
298
+ <p id="fileName" class="text-sm text-gray-600 mt-2"></p>
299
+ </div>
300
+ <button type="submit" class="blue-button w-full py-3 text-base">
301
+ Résoudre le problème
302
+ </button>
303
+ </form>
304
+
305
+ <div id="loader" class="hidden mt-8 text-center">
306
+ <span class="loader"></span>
307
+ <p class="mt-4 text-gray-600">Analyse en cours...</p>
308
+ </div>
309
+
310
+ <section id="solution" class="hidden mt-8 space-y-6">
311
+ <div class="border rounded-lg overflow-hidden bg-gray-50">
312
+ <button id="thoughtsToggle" type="button" class="w-full flex justify-between items-center p-3 hover:bg-gray-100 transition duration-150 ease-in-out" aria-expanded="true">
313
+ <span class="font-medium text-gray-700">Processus de Réflexion</span>
314
+ <div class="flex items-center">
315
+ <span id="timestamp" class="timestamp"></span>
316
+ <svg id="toggleIcon" class="w-5 h-5 text-gray-500 ml-2 transform transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
317
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
318
+ </svg>
319
+ </div>
320
+ </button>
321
+ <div id="thoughtsBox" class="thought-box open">
322
+ <div id="thoughtsContent"></div>
323
+ </div>
324
+ </div>
325
+
326
+ <div class="border rounded-lg shadow-sm overflow-hidden bg-white">
327
+ <div class="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-blue-100">
328
+ <h3 class="text-xl font-semibold text-gray-800">Solution Détaillée</h3>
329
+ </div>
330
+ <div id="answerContent" class="table-responsive">
331
+ </div>
332
+ </div>
333
+ </section>
334
+ </main>
335
+ </div>
336
+
337
+ <div id="savedModal">
338
+ <div id="savedModalContent">
339
+ <header class="flex justify-between items-center">
340
+ <h2 class="text-xl font-semibold text-gray-800">Solutions Sauvegardées</h2>
341
+ <button id="closeSaved" aria-label="Fermer">&times;</button>
342
+ </header>
343
+ <div id="savedListContainer">
344
+ <ul id="savedList" class="space-y-2">
345
+ <li class="text-center text-gray-500 italic">Aucune sauvegarde trouvée.</li>
346
+ </ul>
347
+ </div>
348
+ <footer class="text-center">
349
+ <button id="newExercise" class="blue-button w-full py-2.5 text-base">
350
+ Résoudre un nouvel exercice
351
+ </button>
352
+ </footer>
353
+ </div>
354
+ </div>
355
+
356
+ <script>
357
+ document.addEventListener('DOMContentLoaded', () => {
358
+ // --- DOM Element References ---
359
+ const form = document.getElementById('problemForm');
360
+ const imageInput = document.getElementById('imageInput');
361
+ const loader = document.getElementById('loader');
362
+ const solutionSection = document.getElementById('solution');
363
+ const thoughtsContent = document.getElementById('thoughtsContent');
364
+ const answerContent = document.getElementById('answerContent');
365
+ const thoughtsToggle = document.getElementById('thoughtsToggle');
366
+ const thoughtsBox = document.getElementById('thoughtsBox');
367
+ const toggleIcon = document.getElementById('toggleIcon');
368
+ const imagePreview = document.getElementById('imagePreview');
369
+ const previewImage = document.getElementById('previewImage');
370
+ const fileNameDisplay = document.getElementById('fileName');
371
+ const timestamp = document.getElementById('timestamp');
372
+ const saveButton = document.getElementById('saveButton'); // Reference remains valid
373
+ const openSaved = document.getElementById('openSaved');
374
+ const closeSaved = document.getElementById('closeSaved');
375
+ const savedModal = document.getElementById('savedModal');
376
+ const savedList = document.getElementById('savedList');
377
+ const newExercise = document.getElementById('newExercise');
378
+ const mainContent = document.getElementById('mainContent');
379
+ const dropZone = document.querySelector('.uploadArea');
380
+
381
+ // --- State Variables ---
382
+ let startTime = null;
383
+ let timerInterval = null;
384
+ let thoughtsBuffer = '';
385
+ let answerBuffer = '';
386
+ let currentMode = null;
387
+ let updateTimeout = null;
388
+ let mathJaxProcessing = false;
389
+
390
+ // --- Timer Functions ---
391
+ const updateTimestamp = () => { if (startTime) timestamp.textContent = `${Math.floor((Date.now() - startTime) / 1000)}s`; };
392
+ const startTimer = () => { startTime = Date.now(); timerInterval = setInterval(updateTimestamp, 1000); updateTimestamp(); };
393
+ const stopTimer = () => { clearInterval(timerInterval); };
394
+
395
+ // --- File Handling & Preview (Image Only) ---
396
+ const handleFileSelect = file => {
397
+ // Clear previous preview and name
398
+ previewImage.src = '';
399
+ fileNameDisplay.textContent = '';
400
+ imagePreview.classList.add('hidden');
401
+ imageInput.value = ''; // Clear input value initially
402
+
403
+ if (!file) return;
404
+
405
+ // **Validation: Check if it's an image**
406
+ if (!file.type.startsWith('image/')) {
407
+ showError('Fichier non valide. Veuillez sélectionner une image (jpg, png, gif...).');
408
+ return; // Stop processing if not an image
409
+ }
410
+
411
+ // If valid image, proceed with preview
412
+ fileNameDisplay.textContent = file.name;
413
+ const reader = new FileReader();
414
+ reader.onload = e => {
415
+ previewImage.src = e.target.result;
416
+ imagePreview.classList.remove('hidden');
417
+ // Re-assign the valid file to the input (needed if cleared earlier)
418
+ // This is tricky, usually done via DataTransfer, but simpler to just let the original selection stand if valid
419
+ };
420
+ reader.onerror = () => { showError('Erreur lors de la lecture de l\'image.'); };
421
+ reader.readAsDataURL(file);
422
+ };
423
+
424
+ // --- UI Toggles ---
425
+ thoughtsToggle.addEventListener('click', () => {
426
+ const isOpen = thoughtsBox.classList.toggle('open');
427
+ toggleIcon.style.transform = isOpen ? 'rotate(180deg)' : 'rotate(0deg)';
428
+ thoughtsToggle.setAttribute('aria-expanded', isOpen);
429
+ });
430
+
431
+ // --- Drag and Drop (Image Only Validation) ---
432
+ const handleDragDrop = (e) => {
433
+ e.preventDefault();
434
+ dropZone.classList.remove('border-blue-400', 'bg-blue-50');
435
+ const file = e.dataTransfer.files[0];
436
+ if (file) {
437
+ // Validate type before assigning and handling
438
+ if (file.type.startsWith('image/')) {
439
+ // Create a new FileList to assign to input.files
440
+ const dataTransfer = new DataTransfer();
441
+ dataTransfer.items.add(file);
442
+ imageInput.files = dataTransfer.files;
443
+ handleFileSelect(file); // Handle the valid file
444
+ } else {
445
+ showError('Fichier non valide. Veuillez déposer une image (jpg, png, gif...).');
446
+ imageInput.value = ''; // Clear input if invalid file dropped
447
+ }
448
+ }
449
+ }
450
+
451
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('border-blue-400', 'bg-blue-50'); });
452
+ dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('border-blue-400', 'bg-blue-50'); });
453
+ dropZone.addEventListener('drop', handleDragDrop); // Use updated handler
454
+ dropZone.addEventListener('click', () => { imageInput.click(); });
455
+ imageInput.addEventListener('change', (e) => {
456
+ // Handle selection, validation is inside handleFileSelect
457
+ handleFileSelect(e.target.files[0]);
458
+ });
459
+
460
+
461
+ // --- MathJax Rendering ---
462
+ const typesetContent = async (elementId) => {
463
+ if (!window.mathJaxReady || mathJaxProcessing) return;
464
+ const element = document.getElementById(elementId);
465
+ if (!element || (!element.innerHTML.includes('$') && !element.innerHTML.includes('\\ce{'))) return;
466
+ mathJaxProcessing = true;
467
+ try {
468
+ MathJax.startup.document.clear();
469
+ MathJax.startup.document.elements = [element];
470
+ await MathJax.typesetPromise();
471
+ } catch (error) {
472
+ console.error("MathJax Typesetting Error:", error);
473
+ showError("Erreur lors de l'affichage des formules.");
474
+ } finally {
475
+ mathJaxProcessing = false;
476
+ }
477
+ };
478
+
479
+ // --- Content Update & Display ---
480
+ const updateDisplay = async () => {
481
+ thoughtsContent.innerHTML = marked.parse(thoughtsBuffer);
482
+ answerContent.innerHTML = marked.parse(answerBuffer);
483
+ await typesetContent('answerContent');
484
+ updateTimeout = null;
485
+ };
486
+ const scheduleUpdate = () => { if (!updateTimeout) updateTimeout = requestAnimationFrame(updateDisplay); };
487
+
488
+ // Configure Marked
489
+ marked.setOptions({ gfm: true, breaks: true, mangle: false, headerIds: false });
490
+
491
+ // --- Form Submission (API Call) ---
492
+ form.addEventListener('submit', async e => {
493
+ e.preventDefault();
494
+ const file = imageInput.files[0];
495
+ // Double check file exists and is an image before sending
496
+ if (!file || !file.type.startsWith('image/')) {
497
+ showError('Veuillez sélectionner une image valide.');
498
+ return;
499
+ }
500
+
501
+ // Reset UI state
502
+ startTimer();
503
+ saveButton.disabled = true; // Disable save button on new submission
504
+ loader.classList.remove('hidden');
505
+ solutionSection.classList.add('hidden');
506
+ thoughtsContent.innerHTML = '';
507
+ answerContent.innerHTML = '';
508
+ thoughtsBuffer = '';
509
+ answerBuffer = '';
510
+ currentMode = null;
511
+ if (!thoughtsBox.classList.contains('open')) thoughtsToggle.click();
512
+
513
+ const formData = new FormData();
514
+ formData.append('file', file); // Changed key to 'file'
515
+
516
+ try {
517
+ const response = await fetch('/solve', { method: 'POST', body: formData }); // Replace with your endpoint
518
+
519
+ if (!response.ok) {
520
+ let errorMsg = `Erreur serveur: ${response.status} ${response.statusText}`;
521
+ try { errorMsg = (await response.json()).error || errorMsg; } catch (_) {}
522
+ throw new Error(errorMsg);
523
+ }
524
+ if (!response.body) throw new Error("La réponse du serveur est vide.");
525
+
526
+ const reader = response.body.getReader();
527
+ const decoder = new TextDecoder();
528
+ let buffer = '';
529
+
530
+ // Process stream
531
+ const processChunk = async ({ done, value }) => {
532
+ if (done) {
533
+ if (buffer) processDataLine(buffer); // Process remaining buffer
534
+ scheduleUpdate(); // Final update
535
+ stopTimer();
536
+ loader.classList.add('hidden');
537
+ // **Enable Save Button only if answer content exists**
538
+ if (answerBuffer.trim() !== '') {
539
+ saveButton.disabled = false;
540
+ } else {
541
+ saveButton.disabled = true; // Keep disabled if no answer
542
+ if (!currentMode) showError("Aucune donnée valide reçue du serveur.");
543
+ }
544
+ return;
545
+ }
546
+
547
+ buffer += decoder.decode(value, { stream: true });
548
+ const lines = buffer.split('\n\n');
549
+ buffer = lines.pop();
550
+ lines.forEach(processDataLine);
551
+ scheduleUpdate();
552
+ reader.read().then(processChunk);
553
+ };
554
+
555
+ const processDataLine = (line) => {
556
+ if (!line.startsWith('data:')) return;
557
+ try {
558
+ const data = JSON.parse(line.substring(5).trim());
559
+ if (data.mode) {
560
+ currentMode = data.mode;
561
+ loader.classList.add('hidden');
562
+ solutionSection.classList.remove('hidden');
563
+ }
564
+ if (data.content) {
565
+ if (currentMode === 'thinking') thoughtsBuffer += data.content;
566
+ else if (currentMode === 'answering') answerBuffer += data.content;
567
+ }
568
+ if (data.error) {
569
+ showError(`Erreur de traitement: ${data.error}`);
570
+ stopTimer();
571
+ saveButton.disabled = true; // Ensure disabled on error
572
+ }
573
+ } catch (parseError) { console.error("Erreur de parsing JSON:", parseError, "Ligne:", line); }
574
+ };
575
+
576
+ reader.read().then(processChunk);
577
+
578
+ } catch (error) {
579
+ console.error('Erreur de communication ou traitement:', error);
580
+ showError(`Une erreur est survenue: ${error.message}`);
581
+ loader.classList.add('hidden');
582
+ stopTimer();
583
+ saveButton.disabled = true; // Ensure disabled on fetch error
584
+ }
585
+ });
586
+
587
+ // --- Save Solution ---
588
+ saveButton.addEventListener('click', () => {
589
+ if (saveButton.disabled) return; // Prevent saving if disabled
590
+
591
+ Swal.fire({
592
+ title: 'Nommer cette sauvegarde',
593
+ input: 'text',
594
+ inputPlaceholder: 'Ex: Physique - Circuit RLC',
595
+ showCancelButton: true,
596
+ confirmButtonText: 'Sauvegarder',
597
+ cancelButtonText: 'Annuler',
598
+ inputValidator: (value) => !value && 'Veuillez entrer un nom !'
599
+ }).then((result) => {
600
+ if (result.isConfirmed && result.value) {
601
+ const saveName = result.value;
602
+ const saveData = {
603
+ answer: answerContent.innerHTML,
604
+ thinking: thoughtsContent.innerHTML,
605
+ date: new Date().toLocaleString('fr-FR')
606
+ };
607
+ let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
608
+ savedExercises[saveName] = saveData;
609
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
610
+ Swal.fire({ icon: 'success', title: 'Sauvegardé !', text: `Solution sauvegardée sous "${saveName}".`, timer: 2000, showConfirmButton: false });
611
+ }
612
+ });
613
+ });
614
+
615
+ // --- Load Saved Solutions ---
616
+ const loadSavedList = () => {
617
+ savedList.innerHTML = '';
618
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
619
+ const keys = Object.keys(savedExercises);
620
+ if (keys.length === 0) {
621
+ savedList.innerHTML = '<li class="text-center text-gray-500 italic py-4">Aucune sauvegarde trouvée.</li>';
622
+ return;
623
+ }
624
+ keys.sort((a, b) => a.localeCompare(b, 'fr', { sensitivity: 'base' })).forEach(name => {
625
+ const data = savedExercises[name];
626
+ const li = document.createElement('li');
627
+ li.className = 'border-b border-gray-200 last:border-b-0';
628
+ li.innerHTML = `
629
+ <div class="flex justify-between items-center p-2 hover:bg-gray-50 transition-colors duration-150">
630
+ <button class="text-left text-blue-600 hover:underline flex-grow mr-2 text-sm" data-save="${name}">
631
+ <span class="font-medium">${name}</span> <span class="text-gray-500 text-xs block">(${data.date})</span>
632
+ </button>
633
+ <button class="text-red-500 hover:text-red-700 text-xs font-medium px-2 py-1 rounded hover:bg-red-100 transition-colors duration-150" data-delete="${name}">Supprimer</button>
634
+ </div>`;
635
+ savedList.appendChild(li);
636
+ });
637
+ };
638
+
639
+ // --- Saved List Event Delegation (View/Delete) ---
640
+ savedList.addEventListener('click', async (e) => {
641
+ const saveName = e.target.closest('[data-save]')?.dataset.save;
642
+ const deleteName = e.target.closest('[data-delete]')?.dataset.delete;
643
+
644
+ if (saveName) {
645
+ const savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
646
+ const data = savedExercises[saveName];
647
+ if (data) {
648
+ form.classList.add('hidden');
649
+ loader.classList.add('hidden');
650
+ solutionSection.classList.remove('hidden');
651
+ thoughtsContent.innerHTML = data.thinking;
652
+ answerContent.innerHTML = data.answer;
653
+ const hasThinkingContent = data.thinking && data.thinking.trim() !== '';
654
+ if (hasThinkingContent && !thoughtsBox.classList.contains('open')) thoughtsToggle.click();
655
+ else if (!hasThinkingContent && thoughtsBox.classList.contains('open')) thoughtsToggle.click();
656
+ await typesetContent('answerContent');
657
+ saveButton.disabled = false; // Enable save button when loading a solution
658
+ savedModal.classList.remove('active');
659
+ }
660
+ } else if (deleteName) {
661
+ Swal.fire({
662
+ title: 'Êtes-vous sûr ?', text: `Voulez-vous vraiment supprimer "${deleteName}" ?`, icon: 'warning',
663
+ showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: '#3085d6',
664
+ confirmButtonText: 'Oui, supprimer !', cancelButtonText: 'Annuler'
665
+ }).then((result) => {
666
+ if (result.isConfirmed) {
667
+ let savedExercises = JSON.parse(localStorage.getItem('savedExercises') || '{}');
668
+ delete savedExercises[deleteName];
669
+ localStorage.setItem('savedExercises', JSON.stringify(savedExercises));
670
+ loadSavedList();
671
+ Swal.fire('Supprimé !', 'La sauvegarde a été supprimée.', 'success');
672
+ }
673
+ });
674
+ }
675
+ });
676
+
677
+ // --- Modal Open/Close Logic ---
678
+ openSaved.addEventListener('click', () => { loadSavedList(); savedModal.classList.add('active'); });
679
+ closeSaved.addEventListener('click', () => { savedModal.classList.remove('active'); });
680
+ savedModal.addEventListener('click', (e) => { if (e.target === savedModal) savedModal.classList.remove('active'); });
681
+
682
+ // --- New Exercise Button (inside modal) ---
683
+ newExercise.addEventListener('click', () => {
684
+ form.reset(); // Resets the form including file input
685
+ form.classList.remove('hidden');
686
+ solutionSection.classList.add('hidden');
687
+ imagePreview.classList.add('hidden');
688
+ previewImage.src = '';
689
+ fileNameDisplay.textContent = '';
690
+ thoughtsContent.innerHTML = '';
691
+ answerContent.innerHTML = '';
692
+ thoughtsBuffer = '';
693
+ answerBuffer = '';
694
+ timestamp.textContent = '';
695
+ startTime = null;
696
+ saveButton.disabled = true; // Disable save button for new exercise
697
+ savedModal.classList.remove('active');
698
+ });
699
+
700
+ // --- Utility: Show Error Messages ---
701
+ const showError = (message) => { Swal.fire({ icon: 'error', title: 'Erreur', text: message, confirmButtonColor: '#3b82f6' }); };
702
+
703
+ // --- Initial Setup ---
704
+ saveButton.disabled = true; // Ensure save button is disabled on initial load
705
+
706
+ }); // End DOMContentLoaded
707
+ </script>
708
+
709
+ </body>
710
+ </html>