Docfile commited on
Commit
94f2291
·
verified ·
1 Parent(s): 734a678

Update templates/svt.html

Browse files
Files changed (1) hide show
  1. templates/svt.html +412 -434
templates/svt.html CHANGED
@@ -1,466 +1,444 @@
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 AI - SVT</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script>
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
10
- <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
11
- <style>
12
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
13
-
14
- body {
15
- font-family: 'Poppins', sans-serif;
16
- background: linear-gradient(135deg, #f6f8fc 0%, #e9f0f7 100%);
17
- }
18
-
19
- .animate-fade-in {
20
- animation: fadeIn 0.5s ease-in;
21
- }
22
-
23
- .animate-slide-up {
24
- animation: slideUp 0.5s ease-out;
25
- }
26
-
27
- @keyframes fadeIn {
28
- from { opacity: 0; }
29
- to { opacity: 1; }
30
- }
31
-
32
- @keyframes slideUp {
33
- from { transform: translateY(20px); opacity: 0; }
34
- to { transform: translateY(0); opacity: 1; }
35
- }
36
-
37
- .glass-effect {
38
- background: rgba(255, 255, 255, 0.95);
39
- backdrop-filter: blur(10px);
40
- border: 1px solid rgba(255, 255, 255, 0.2);
41
- }
42
-
43
- .hover-scale {
44
- transition: transform 0.2s;
45
- }
46
-
47
- .hover-scale:hover {
48
- transform: scale(1.02);
49
- }
50
-
51
- .image-preview {
52
- transition: all 0.3s ease;
53
- }
54
-
55
- .image-preview:hover .image-overlay {
56
- opacity: 1;
57
- }
58
-
59
- .image-overlay {
60
- opacity: 0;
61
- transition: opacity 0.3s ease;
62
- background: rgba(0, 0, 0, 0.5);
63
- }
64
 
65
- .preview-container {
66
- display: grid;
67
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
68
- gap: 1rem;
69
- }
 
 
 
 
 
 
 
 
 
 
70
 
71
- /* Responsive adjustments */
72
- @media (max-width: 640px) {
73
- .preview-container {
74
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
75
- }
76
- .image-preview img {
77
- height: 32 object-cover;
78
- }
79
- #historyContainer .grid {
80
- grid-template-columns: repeat(1, minmax(0, 1fr));
81
- }
82
- }
83
 
84
- #response {
85
- white-space: pre-wrap; /* Préserve les espaces et les sauts de ligne */
86
- word-wrap: break-word; /* Permet le retour à la ligne des mots longs */
87
- overflow-wrap: break-word; /* Assure la césure des mots très longs */
88
- }
89
-
90
- /* Styles pour le contenu markdown */
91
- #response p {
92
- margin-bottom: 1em;
93
- line-height: 1.6;
94
- }
95
-
96
- #response ul, #response ol {
97
- padding-left: 1.5em;
98
- margin-bottom: 1em;
99
- }
100
-
101
- #response li {
102
- margin-bottom: 0.5em;
103
- }
104
-
105
- /* Ajustements responsives pour mobile */
106
- @media (max-width: 640px) {
107
- #response {
108
- font-size: 0.95rem;
109
- padding: 1rem;
110
  }
111
-
112
- #response p, #response li {
113
- line-height: 1.7;
114
  }
115
-
116
- #response ul, #response ol {
117
- padding-left: 1.2em;
 
118
  }
119
- }
120
 
121
-
122
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  </head>
124
  <body class="min-h-screen flex items-center justify-center p-4 md:p-8">
125
- <div class="container mx-auto glass-effect rounded-2xl shadow-xl max-w-4xl animate-fade-in p-6 md:p-8">
126
- <header class="flex flex-col md:flex-row justify-between items-center mb-8">
127
- <div class="text-center md:text-left mb-4 md:mb-0">
128
- <h1 class="text-4xl font-bold text-blue-900">Mariam AI</h1>
129
- <p class="text-gray-600 mt-2">Assistant SVT Intelligent</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  </div>
131
- <button onclick="showInfo()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl hover-scale transition-all duration-300 flex items-center gap-2">
132
- <i class="fas fa-info-circle"></i>
133
- <span>Guide d'utilisation</span>
134
- </button>
135
- </header>
136
-
137
- <div class="space-y-8 animate-slide-up">
138
- <div class="relative">
139
- <label for="svtOption" class="block mb-3 text-lg font-medium text-gray-700">Type d'exercice :</label>
140
- <select id="svtOption" class="w-full p-4 border border-gray-200 rounded-xl bg-white shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300">
141
- <option value="Restitution organisée des connaissances">Restitution Organisée des Connaissances</option>
142
- <option value="Exploitation du document">Exploitation du Document</option>
143
- <option value="Synthèse">Synthèse</option>
144
- </select>
145
  </div>
 
 
 
 
 
 
146
 
147
- <div class="relative">
148
- <label class="block mb-3 text-lg font-medium text-gray-700">Images du sujet :</label>
149
- <div class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center hover:border-blue-500 transition-all duration-300">
150
- <input type="file" id="imageUpload" class="hidden" multiple accept="image/*" onchange="handleImageUpload(event)">
151
- <label for="imageUpload" class="cursor-pointer">
152
- <i class="fas fa-cloud-upload-alt text-4xl text-blue-500 mb-4"></i>
153
- <p class="text-gray-600">Glissez vos images ici ou cliquez pour sélectionner</p>
154
- <p class="text-sm text-gray-500 mt-2">Format acceptés : JPG, PNG, GIF</p>
155
- </label>
156
- </div>
157
-
158
- <!-- Conteneur des prévisualisations -->
159
- <div id="previewContainer" class="preview-container mt-4">
160
- <!-- Les prévisualisations seront ajoutées ici -->
161
- </div>
162
- </div>
163
 
164
- <button onclick="submitQuestion()" class="w-full bg-gradient-to-r from-blue-600 to-blue-800 text-white py-4 rounded-xl hover-scale transition-all duration-300 font-semibold text-lg flex items-center justify-center gap-2">
165
- <i class="fas fa-paper-plane"></i>
166
- Analyser
167
- </button>
 
 
 
 
 
 
 
168
 
169
- <div id="loader" class="hidden">
170
- <div class="flex flex-col items-center space-y-4 p-8">
171
- <div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
172
- <p class="text-gray-600 text-lg">Analyse en cours...</p>
173
- </div>
 
 
 
 
174
  </div>
175
-
176
- <div id="response" class="mt-6 p-6 bg-white rounded-xl shadow-sm prose max-w-none"></div>
177
-
178
- <div id="copyResponseContainer" class="hidden mb-8">
179
- <button onclick="copyResponse()" class="w-full bg-gray-800 hover:bg-gray-900 text-white px-6 py-3 rounded-xl hover-scale transition-all duration-300 flex items-center justify-center gap-2">
180
- <i class="fas fa-copy"></i>
181
- Copier la réponse
182
- </button>
183
  </div>
184
- </div>
185
-
186
- <!-- Historique des réponses déplacé ici -->
187
- <div class="space-y-8 animate-slide-up">
188
- <h2 class="text-2xl font-bold text-blue-900 mb-4">Historique des Réponses</h2>
189
- <button onclick="clearLocalStorage()" class="bg-red-500 hover:bg-red-700 text-white px-4 py-2 rounded-lg">
190
- Effacer l'historique
191
- </button>
192
- <div id="historyContainer">
193
- <!-- L'historique des réponses sera affiché ici -->
194
  </div>
195
- </div>
196
- </div>
197
-
198
-
199
-
200
- <script>
201
- let uploadedFiles = [];
202
-
203
- function handleImageUpload(event) {
204
- const files = event.target.files;
205
- const previewContainer = document.getElementById('previewContainer');
206
-
207
- for (let i = 0; i < files.length; i++) {
208
- const file = files[i];
209
- uploadedFiles.push(file);
210
-
211
- const reader = new FileReader();
212
- reader.onload = function(e) {
213
- const imageId = `img-${Date.now()}-${i}`;
214
- const previewDiv = document.createElement('div');
215
- previewDiv.className = 'image-preview relative rounded-lg overflow-hidden';
216
- previewDiv.id = imageId;
217
- previewDiv.innerHTML = `
218
- <img src="${e.target.result}" alt="${file.name}" class="w-full h-40 object-cover">
219
- <div class="image-overlay absolute inset-0 flex items-center justify-center">
220
- <button onclick="removeImage('${imageId}')" class="bg-red-500 hover:bg-red-600 text-white p-2 rounded-full transition-all duration-300">
221
- <i class="fas fa-trash"></i>
222
- </button>
223
- <button onclick="previewImage('${e.target.result}')" class="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-full ml-2 transition-all duration-300">
224
- <i class="fas fa-eye"></i>
225
- </button>
226
- </div>
227
- <div class="bg-black bg-opacity-50 text-white text-xs p-1 absolute bottom-0 left-0 right-0">
228
- ${file.name.substring(0, 15)}${file.name.length > 15 ? '...' : ''}
229
- </div>
230
- `;
231
- previewContainer.appendChild(previewDiv);
232
- };
233
- reader.readAsDataURL(file);
234
- }
235
- }
236
-
237
- function removeImage(imageId) {
238
- const imageIndex = uploadedFiles.findIndex(file => {
239
- const fileId = `img-${file.lastModified}-${uploadedFiles.indexOf(file)}`;
240
- return fileId === imageId;
241
- });
242
-
243
- if (imageIndex !== -1) {
244
- uploadedFiles.splice(imageIndex, 1);
245
- }
246
-
247
- const element = document.getElementById(imageId);
248
- if (element) {
249
- element.remove();
250
- }
251
- }
252
-
253
- function previewImage(src) {
254
- Swal.fire({
255
- imageUrl: src,
256
- imageAlt: 'Prévisualisation',
257
- width: '80%',
258
- showConfirmButton: false,
259
- showCloseButton: true,
260
- customClass: {
261
- image: 'max-h-[80vh] object-contain'
262
- }
263
- });
264
- }
265
-
266
- function showInfo() {
267
- Swal.fire({
268
- title: 'Guide d\'utilisation',
269
- html: `
270
- <div class="text-left space-y-4">
271
- <div class="flex items-start gap-3">
272
- <i class="fas fa-check-circle text-green-500 mt-1"></i>
273
- <p>Sélectionnez le type d'exercice correspondant à votre sujet.</p>
274
- </div>
275
- <div class="flex items-start gap-3">
276
- <i class="fas fa-image text-blue-500 mt-1"></i>
277
- <p>Assurez-vous que vos images sont nettes et bien cadrées.</p>
278
- </div>
279
- <div class="flex items-start gap-3">
280
- <i class="fas fa-crop-alt text-purple-500 mt-1"></i>
281
- <p>Rognez vos images pour ne garder que l'essentiel du sujet.</p>
282
- </div>
283
- </div>
284
- `,
285
- icon: 'info',
286
- confirmButtonText: 'Compris',
287
- confirmButtonColor: '#2563eb',
288
- customClass: {
289
- container: 'font-sans'
290
- }
291
- });
292
- }
293
 
294
- function copyResponse() {
295
- const responseDiv = document.getElementById('response');
296
- const range = document.createRange();
297
- range.selectNode(responseDiv);
298
- window.getSelection().removeAllRanges();
299
- window.getSelection().addRange(range);
300
- document.execCommand('copy');
301
- window.getSelection().removeAllRanges();
302
-
303
- Swal.fire({
304
- icon: 'success',
305
- title: 'Copié !',
306
- text: 'La réponse a été copiée dans le presse-papiers.',
307
- showConfirmButton: false,
308
- timer: 1500,
309
- customClass: {
310
- popup: 'animate-fade-in'
311
- }
312
- });
313
- }
314
 
315
- async function submitQuestion() {
316
- if (uploadedFiles.length === 0) {
317
- Swal.fire({
318
- icon: 'error',
319
- title: 'Images manquantes',
320
- text: 'Veuillez sélectionner au moins une image du sujet.',
321
- confirmButtonColor: '#2563eb'
322
- });
323
- return;
324
- }
325
-
326
- const option = document.getElementById('svtOption').value;
327
- const loader = document.getElementById('loader');
328
- const responseDiv = document.getElementById('response');
329
- const copyResponseContainer = document.getElementById('copyResponseContainer');
330
-
331
- loader.classList.remove('hidden');
332
- responseDiv.innerHTML = '';
333
- copyResponseContainer.classList.add('hidden');
334
-
335
- const formData = new FormData();
336
- formData.append('option', option);
337
-
338
- for (let i = 0; i < uploadedFiles.length; i++) {
339
- formData.append('images', uploadedFiles[i]);
340
- }
341
-
342
- const response = await fetch('/svt_submit', {
343
- method: 'POST',
344
- body: formData
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  });
346
-
347
- const data = await response.json();
348
- loader.classList.add('hidden');
349
-
350
- if (data.error) {
351
- responseDiv.innerHTML = `
352
- <div class="bg-red-50 border-l-4 border-red-500 p-4 rounded">
353
- <p class="text-red-700">Erreur : ${data.error}</p>
354
- </div>
355
- `;
356
- } else {
357
- const htmlContent = marked.parse(data.response);
358
- responseDiv.innerHTML = htmlContent;
359
- responseDiv.classList.add('animate-fade-in');
360
- copyResponseContainer.classList.remove('hidden');
361
-
362
- // Convertir les images en base64 avant de les sauvegarder
363
- const imagesData = await Promise.all(uploadedFiles.map(file => {
364
- return new Promise((resolve) => {
365
- const reader = new FileReader();
366
- reader.onload = (e) => resolve(e.target.result);
367
- reader.readAsDataURL(file);
368
- });
369
- }));
370
-
371
- saveResponseToLocalStorage(option, imagesData, data.response);
372
- displayHistory();
373
- }
374
  }
 
 
 
 
 
 
 
 
 
375
 
376
- // Fonctions pour la gestion du localStorage
377
- function saveResponseToLocalStorage(option, images, response) {
378
- const timestamp = new Date().toISOString();
379
- const data = { option, images, response, timestamp };
380
- localStorage.setItem('svt_response_' + timestamp, JSON.stringify(data));
381
- }
382
 
383
- function loadResponsesFromLocalStorage() {
384
- const responses = [];
385
- for (let i = 0; i < localStorage.length; i++) {
386
- const key = localStorage.key(i);
387
- if (key.startsWith('svt_response_')) {
388
- const data = JSON.parse(localStorage.getItem(key));
389
- responses.push(data);
390
- }
391
- }
392
- return responses.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
393
  }
 
 
 
394
 
395
- function clearLocalStorage() {
396
- const keysToRemove = [];
397
- for (let i = 0; i < localStorage.length; i++) {
398
- const key = localStorage.key(i);
399
- if (key.startsWith('svt_response_')) {
400
- keysToRemove.push(key);
401
- }
402
- }
403
- for (const key of keysToRemove) {
404
- localStorage.removeItem(key);
405
- }
406
- displayHistory();
407
  }
 
 
 
 
408
 
409
- function displayHistory() {
410
- const responses = loadResponsesFromLocalStorage();
411
- const historyContainer = document.getElementById('historyContainer');
412
- historyContainer.innerHTML = '';
413
-
414
- if (responses.length === 0) {
415
- historyContainer.innerHTML = '<p class="text-gray-500">Aucun historique disponible.</p>';
416
- return;
417
- }
418
-
419
- const responseList = document.createElement('ul');
420
- responseList.className = 'grid gap-4 md:grid-cols-2';
421
-
422
- responses.forEach(response => {
423
- const listItem = document.createElement('li');
424
- listItem.className = 'bg-white p-4 rounded-lg shadow-md hover:shadow-lg transition duration-300';
425
-
426
- const title = document.createElement('h4');
427
- title.className = 'text-lg font-semibold text-blue-800 mb-2';
428
- title.textContent = `${response.option} - ${new Date(response.timestamp).toLocaleString()}`;
429
- listItem.appendChild(title);
430
-
431
- const previewContainer = document.createElement('div');
432
- previewContainer.className = 'flex gap-2 mb-2';
433
- response.images.forEach(imageData => {
434
- const img = document.createElement('img');
435
- img.src = imageData;
436
- img.className = 'h-12 w-12 object-cover rounded-md cursor-pointer';
437
- img.onclick = () => previewImage(imageData);
438
- previewContainer.appendChild(img);
439
- });
440
- listItem.appendChild(previewContainer);
441
-
442
- const responsePreview = document.createElement('p');
443
- responsePreview.className = 'text-gray-600 text-sm';
444
- responsePreview.textContent = response.response.substring(0, 200) + (response.response.length > 200 ? '...' : '');
445
- listItem.appendChild(responsePreview);
446
-
447
- const viewButton = document.createElement('button');
448
- viewButton.className = 'bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg mt-2 text-sm';
449
- viewButton.textContent = 'Voir';
450
- viewButton.onclick = () => {
451
- document.getElementById('response').innerHTML = marked.parse(response.response);
452
- document.getElementById('copyResponseContainer').classList.remove('hidden');
453
- window.scrollTo({ top: document.getElementById('response').offsetTop, behavior: 'smooth' });
454
- };
455
- listItem.appendChild(viewButton);
456
-
457
- responseList.appendChild(listItem);
458
- });
459
-
460
- historyContainer.appendChild(responseList);
461
- }
 
 
462
 
463
- displayHistory();
464
- </script>
 
 
 
 
 
 
 
 
465
  </body>
466
- </html>
 
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 AI - SVT</title>
7
+ <!-- Tailwind CSS, Font Awesome, marked et SweetAlert2 -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js" defer></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11" defer></script>
12
+ <style>
13
+ /* Importation de la police Poppins */
14
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
15
+
16
+ /* Style de base */
17
+ body {
18
+ font-family: 'Poppins', sans-serif;
19
+ background: linear-gradient(135deg, #f6f8fc 0%, #e9f0f7 100%);
20
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ /* Animations */
23
+ .animate-fade-in {
24
+ animation: fadeIn 0.5s ease-in;
25
+ }
26
+ .animate-slide-up {
27
+ animation: slideUp 0.5s ease-out;
28
+ }
29
+ @keyframes fadeIn {
30
+ from { opacity: 0; }
31
+ to { opacity: 1; }
32
+ }
33
+ @keyframes slideUp {
34
+ from { transform: translateY(20px); opacity: 0; }
35
+ to { transform: translateY(0); opacity: 1; }
36
+ }
37
 
38
+ /* Effet verre (glass effect) */
39
+ .glass-effect {
40
+ background: rgba(255, 255, 255, 0.95);
41
+ backdrop-filter: blur(10px);
42
+ border: 1px solid rgba(255, 255, 255, 0.2);
43
+ }
 
 
 
 
 
 
44
 
45
+ /* Prévisualisation des images */
46
+ .image-preview {
47
+ position: relative;
48
+ border-radius: 0.5rem;
49
+ overflow: hidden;
50
+ cursor: pointer;
51
+ }
52
+ .image-preview img {
53
+ width: 100%;
54
+ height: 10rem;
55
+ object-fit: cover;
56
+ display: block;
57
+ }
58
+ .image-overlay {
59
+ position: absolute;
60
+ inset: 0;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ background: rgba(0, 0, 0, 0.5);
65
+ opacity: 0;
66
+ transition: opacity 0.3s ease;
 
 
 
 
67
  }
68
+ .image-preview:hover .image-overlay {
69
+ opacity: 1;
 
70
  }
71
+ .preview-container {
72
+ display: grid;
73
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
74
+ gap: 1rem;
75
  }
 
76
 
77
+ /* Styles du contenu markdown */
78
+ #response {
79
+ white-space: pre-wrap;
80
+ word-wrap: break-word;
81
+ overflow-wrap: break-word;
82
+ }
83
+ #response p { margin-bottom: 1em; line-height: 1.6; }
84
+ #response ul,
85
+ #response ol { padding-left: 1.5em; margin-bottom: 1em; }
86
+ #response li { margin-bottom: 0.5em; }
87
+
88
+ /* Ajustements responsives */
89
+ @media (max-width: 640px) {
90
+ .preview-container { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
91
+ #response { font-size: 0.95rem; padding: 1rem; }
92
+ #response p, #response li { line-height: 1.7; }
93
+ #response ul, #response ol { padding-left: 1.2em; }
94
+ }
95
+ </style>
96
  </head>
97
  <body class="min-h-screen flex items-center justify-center p-4 md:p-8">
98
+ <main class="container mx-auto glass-effect rounded-2xl shadow-xl max-w-4xl p-6 md:p-8 animate-fade-in">
99
+ <!-- En-tête -->
100
+ <header class="flex flex-col md:flex-row justify-between items-center mb-8">
101
+ <div class="text-center md:text-left mb-4 md:mb-0">
102
+ <h1 class="text-4xl font-bold text-blue-900">Mariam AI</h1>
103
+ <p class="text-gray-600 mt-2">Assistant SVT Intelligent</p>
104
+ </div>
105
+ <button id="infoButton" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl transition-transform transform hover:scale-105 flex items-center gap-2" aria-label="Afficher le guide d'utilisation">
106
+ <i class="fas fa-info-circle"></i>
107
+ <span>Guide d'utilisation</span>
108
+ </button>
109
+ </header>
110
+
111
+ <!-- Section principale -->
112
+ <section id="formSection" class="space-y-8 animate-slide-up">
113
+ <!-- Sélection du type d'exercice -->
114
+ <div>
115
+ <label for="svtOption" class="block mb-3 text-lg font-medium text-gray-700">Type d'exercice :</label>
116
+ <select id="svtOption" class="w-full p-4 border border-gray-200 rounded-xl shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent transition">
117
+ <option value="Restitution organisée des connaissances">Restitution Organisée des Connaissances</option>
118
+ <option value="Exploitation du document">Exploitation du Document</option>
119
+ <option value="Synthèse">Synthèse</option>
120
+ </select>
121
+ </div>
122
+
123
+ <!-- Upload et prévisualisation des images -->
124
+ <div>
125
+ <label for="imageUpload" class="block mb-3 text-lg font-medium text-gray-700">Images du sujet :</label>
126
+ <div id="imageDropzone" class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center hover:border-blue-500 transition cursor-pointer">
127
+ <input type="file" id="imageUpload" class="hidden" multiple accept="image/*">
128
+ <p><i class="fas fa-cloud-upload-alt text-4xl text-blue-500 mb-4"></i></p>
129
+ <p class="text-gray-600">Glissez vos images ici ou cliquez pour sélectionner</p>
130
+ <p class="text-sm text-gray-500 mt-2">Formats acceptés : JPG, PNG, GIF</p>
131
+ </div>
132
+ <!-- Conteneur pour les images prévisualisées -->
133
+ <div id="previewContainer" class="preview-container mt-4"></div>
134
+ </div>
135
+
136
+ <!-- Bouton d'analyse -->
137
+ <button id="submitButton" class="w-full bg-gradient-to-r from-blue-600 to-blue-800 text-white py-4 rounded-xl font-semibold text-lg flex items-center justify-center gap-2 transition-transform transform hover:scale-105">
138
+ <i class="fas fa-paper-plane"></i>
139
+ Analyser
140
+ </button>
141
+
142
+ <!-- Loader pendant l'analyse -->
143
+ <div id="loader" class="hidden">
144
+ <div class="flex flex-col items-center space-y-4 p-8">
145
+ <div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
146
+ <p class="text-gray-600 text-lg">Analyse en cours...</p>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Affichage de la réponse -->
151
+ <div id="response" class="mt-6 p-6 bg-white rounded-xl shadow-sm prose max-w-none"></div>
152
+ <div id="copyResponseContainer" class="hidden mb-8">
153
+ <button id="copyButton" class="w-full bg-gray-800 hover:bg-gray-900 text-white px-6 py-3 rounded-xl flex items-center justify-center gap-2 transition-transform transform hover:scale-105" aria-label="Copier la réponse">
154
+ <i class="fas fa-copy"></i>
155
+ Copier la réponse
156
+ </button>
157
+ </div>
158
+ </section>
159
+
160
+ <!-- Historique des réponses -->
161
+ <section id="historySection" class="space-y-8 animate-slide-up mt-8">
162
+ <h2 class="text-2xl font-bold text-blue-900 mb-4">Historique des Réponses</h2>
163
+ <button id="clearHistoryButton" class="bg-red-500 hover:bg-red-700 text-white px-4 py-2 rounded-lg" aria-label="Effacer l'historique">
164
+ Effacer l'historique
165
+ </button>
166
+ <div id="historyContainer"></div>
167
+ </section>
168
+ </main>
169
+
170
+ <script>
171
+ // Variables globales
172
+ let uploadedFiles = [];
173
+
174
+ // Gestion de l'upload d'images
175
+ function handleImageUpload(event) {
176
+ const files = event.target.files;
177
+ const previewContainer = document.getElementById('previewContainer');
178
+
179
+ for (let i = 0; i < files.length; i++) {
180
+ const file = files[i];
181
+ uploadedFiles.push(file);
182
+ const reader = new FileReader();
183
+ reader.onload = function(e) {
184
+ const imageId = `img-${Date.now()}-${i}`;
185
+ const previewDiv = document.createElement('div');
186
+ previewDiv.className = 'image-preview';
187
+ previewDiv.id = imageId;
188
+ previewDiv.innerHTML = `
189
+ <img src="${e.target.result}" alt="${file.name}">
190
+ <div class="image-overlay flex items-center justify-center gap-2">
191
+ <button onclick="removeImage('${imageId}')" class="bg-red-500 hover:bg-red-600 text-white p-2 rounded-full transition">
192
+ <i class="fas fa-trash"></i>
193
+ </button>
194
+ <button onclick="previewImage('${e.target.result}')" class="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded-full transition">
195
+ <i class="fas fa-eye"></i>
196
+ </button>
197
  </div>
198
+ <div class="bg-black bg-opacity-50 text-white text-xs p-1 absolute bottom-0 left-0 right-0">
199
+ ${file.name.substring(0, 15)}${file.name.length > 15 ? '...' : ''}
 
 
 
 
 
 
 
 
 
 
 
 
200
  </div>
201
+ `;
202
+ previewContainer.appendChild(previewDiv);
203
+ };
204
+ reader.readAsDataURL(file);
205
+ }
206
+ }
207
 
208
+ // Suppression d'une image prévisualisée
209
+ function removeImage(imageId) {
210
+ // Supprime la prévisualisation
211
+ const element = document.getElementById(imageId);
212
+ if (element) { element.remove(); }
213
+ // Mise à jour du tableau uploadedFiles
214
+ uploadedFiles = uploadedFiles.filter(file => {
215
+ // Utilisation d'une correspondance approximative sur le nom du fichier (peut être améliorée)
216
+ return !element?.innerHTML.includes(file.name);
217
+ });
218
+ }
 
 
 
 
 
219
 
220
+ // Prévisualisation en grand avec SweetAlert2
221
+ function previewImage(src) {
222
+ Swal.fire({
223
+ imageUrl: src,
224
+ imageAlt: 'Prévisualisation',
225
+ width: '80%',
226
+ showConfirmButton: false,
227
+ showCloseButton: true,
228
+ customClass: { image: 'max-h-[80vh] object-contain' }
229
+ });
230
+ }
231
 
232
+ // Affichage du guide d'utilisation
233
+ function showInfo() {
234
+ Swal.fire({
235
+ title: 'Guide d\'utilisation',
236
+ html: `
237
+ <div class="text-left space-y-4">
238
+ <div class="flex items-start gap-3">
239
+ <i class="fas fa-check-circle text-green-500 mt-1"></i>
240
+ <p>Sélectionnez le type d'exercice correspondant à votre sujet.</p>
241
  </div>
242
+ <div class="flex items-start gap-3">
243
+ <i class="fas fa-image text-blue-500 mt-1"></i>
244
+ <p>Assurez-vous que vos images sont nettes et bien cadrées.</p>
 
 
 
 
 
245
  </div>
246
+ <div class="flex items-start gap-3">
247
+ <i class="fas fa-crop-alt text-purple-500 mt-1"></i>
248
+ <p>Rognez vos images pour ne garder que l'essentiel du sujet.</p>
 
 
 
 
 
 
 
249
  </div>
250
+ </div>
251
+ `,
252
+ icon: 'info',
253
+ confirmButtonText: 'Compris',
254
+ confirmButtonColor: '#2563eb'
255
+ });
256
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
+ // Copie du contenu de la réponse
259
+ function copyResponse() {
260
+ const responseDiv = document.getElementById('response');
261
+ const range = document.createRange();
262
+ range.selectNode(responseDiv);
263
+ window.getSelection().removeAllRanges();
264
+ window.getSelection().addRange(range);
265
+ document.execCommand('copy');
266
+ window.getSelection().removeAllRanges();
267
+
268
+ Swal.fire({
269
+ icon: 'success',
270
+ title: 'Copié !',
271
+ text: 'La réponse a été copiée dans le presse-papiers.',
272
+ showConfirmButton: false,
273
+ timer: 1500
274
+ });
275
+ }
 
 
276
 
277
+ // Envoi des données et récupération de la réponse
278
+ async function submitQuestion() {
279
+ if (uploadedFiles.length === 0) {
280
+ Swal.fire({
281
+ icon: 'error',
282
+ title: 'Images manquantes',
283
+ text: 'Veuillez sélectionner au moins une image du sujet.',
284
+ confirmButtonColor: '#2563eb'
285
+ });
286
+ return;
287
+ }
288
+
289
+ const option = document.getElementById('svtOption').value;
290
+ const loader = document.getElementById('loader');
291
+ const responseDiv = document.getElementById('response');
292
+ const copyResponseContainer = document.getElementById('copyResponseContainer');
293
+
294
+ loader.classList.remove('hidden');
295
+ responseDiv.innerHTML = '';
296
+ copyResponseContainer.classList.add('hidden');
297
+
298
+ const formData = new FormData();
299
+ formData.append('option', option);
300
+ uploadedFiles.forEach(file => formData.append('images', file));
301
+
302
+ try {
303
+ const res = await fetch('/svt_submit', {
304
+ method: 'POST',
305
+ body: formData
306
+ });
307
+ const data = await res.json();
308
+ loader.classList.add('hidden');
309
+
310
+ if (data.error) {
311
+ responseDiv.innerHTML = `
312
+ <div class="bg-red-50 border-l-4 border-red-500 p-4 rounded">
313
+ <p class="text-red-700">Erreur : ${data.error}</p>
314
+ </div>
315
+ `;
316
+ } else {
317
+ // Conversion du markdown en HTML
318
+ const htmlContent = marked.parse(data.response);
319
+ responseDiv.innerHTML = htmlContent;
320
+ responseDiv.classList.add('animate-fade-in');
321
+ copyResponseContainer.classList.remove('hidden');
322
+
323
+ // Sauvegarde dans le localStorage avec les images en base64
324
+ const imagesData = await Promise.all(uploadedFiles.map(file => {
325
+ return new Promise((resolve) => {
326
+ const reader = new FileReader();
327
+ reader.onload = (e) => resolve(e.target.result);
328
+ reader.readAsDataURL(file);
329
  });
330
+ }));
331
+ saveResponseToLocalStorage(option, imagesData, data.response);
332
+ displayHistory();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
+ } catch (error) {
335
+ loader.classList.add('hidden');
336
+ Swal.fire({
337
+ icon: 'error',
338
+ title: 'Erreur réseau',
339
+ text: 'Une erreur est survenue lors de la soumission de la requête.'
340
+ });
341
+ }
342
+ }
343
 
344
+ // Sauvegarde de la réponse dans le localStorage
345
+ function saveResponseToLocalStorage(option, images, response) {
346
+ const timestamp = new Date().toISOString();
347
+ const data = { option, images, response, timestamp };
348
+ localStorage.setItem('svt_response_' + timestamp, JSON.stringify(data));
349
+ }
350
 
351
+ // Chargement des réponses sauvegardées
352
+ function loadResponsesFromLocalStorage() {
353
+ const responses = [];
354
+ for (let i = 0; i < localStorage.length; i++) {
355
+ const key = localStorage.key(i);
356
+ if (key.startsWith('svt_response_')) {
357
+ const data = JSON.parse(localStorage.getItem(key));
358
+ responses.push(data);
 
 
359
  }
360
+ }
361
+ return responses.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
362
+ }
363
 
364
+ // Effacer l'historique dans le localStorage
365
+ function clearLocalStorage() {
366
+ const keysToRemove = [];
367
+ for (let i = 0; i < localStorage.length; i++) {
368
+ const key = localStorage.key(i);
369
+ if (key.startsWith('svt_response_')) {
370
+ keysToRemove.push(key);
 
 
 
 
 
371
  }
372
+ }
373
+ keysToRemove.forEach(key => localStorage.removeItem(key));
374
+ displayHistory();
375
+ }
376
 
377
+ // Affichage de l'historique des réponses
378
+ function displayHistory() {
379
+ const responses = loadResponsesFromLocalStorage();
380
+ const historyContainer = document.getElementById('historyContainer');
381
+ historyContainer.innerHTML = '';
382
+
383
+ if (responses.length === 0) {
384
+ historyContainer.innerHTML = '<p class="text-gray-500">Aucun historique disponible.</p>';
385
+ return;
386
+ }
387
+
388
+ const responseList = document.createElement('ul');
389
+ responseList.className = 'grid gap-4 md:grid-cols-2';
390
+
391
+ responses.forEach(response => {
392
+ const listItem = document.createElement('li');
393
+ listItem.className = 'bg-white p-4 rounded-lg shadow-md hover:shadow-lg transition duration-300';
394
+
395
+ const title = document.createElement('h4');
396
+ title.className = 'text-lg font-semibold text-blue-800 mb-2';
397
+ title.textContent = `${response.option} - ${new Date(response.timestamp).toLocaleString()}`;
398
+ listItem.appendChild(title);
399
+
400
+ const previewContainer = document.createElement('div');
401
+ previewContainer.className = 'flex gap-2 mb-2';
402
+ response.images.forEach(imageData => {
403
+ const img = document.createElement('img');
404
+ img.src = imageData;
405
+ img.className = 'h-12 w-12 object-cover rounded-md cursor-pointer';
406
+ img.alt = 'Prévisualisation';
407
+ img.onclick = () => previewImage(imageData);
408
+ previewContainer.appendChild(img);
409
+ });
410
+ listItem.appendChild(previewContainer);
411
+
412
+ const responsePreview = document.createElement('p');
413
+ responsePreview.className = 'text-gray-600 text-sm';
414
+ responsePreview.textContent = response.response.substring(0, 200) + (response.response.length > 200 ? '...' : '');
415
+ listItem.appendChild(responsePreview);
416
+
417
+ const viewButton = document.createElement('button');
418
+ viewButton.className = 'bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg mt-2 text-sm';
419
+ viewButton.textContent = 'Voir';
420
+ viewButton.onclick = () => {
421
+ document.getElementById('response').innerHTML = marked.parse(response.response);
422
+ document.getElementById('copyResponseContainer').classList.remove('hidden');
423
+ window.scrollTo({ top: document.getElementById('response').offsetTop, behavior: 'smooth' });
424
+ };
425
+ listItem.appendChild(viewButton);
426
+
427
+ responseList.appendChild(listItem);
428
+ });
429
+
430
+ historyContainer.appendChild(responseList);
431
+ }
432
 
433
+ // Ajout des écouteurs d'événements une fois le DOM chargé
434
+ document.addEventListener('DOMContentLoaded', () => {
435
+ document.getElementById('infoButton').addEventListener('click', showInfo);
436
+ document.getElementById('submitButton').addEventListener('click', submitQuestion);
437
+ document.getElementById('imageUpload').addEventListener('change', handleImageUpload);
438
+ document.getElementById('clearHistoryButton').addEventListener('click', clearLocalStorage);
439
+ document.getElementById('copyButton').addEventListener('click', copyResponse);
440
+ displayHistory();
441
+ });
442
+ </script>
443
  </body>
444
+ </html>