Docfile commited on
Commit
473683c
·
verified ·
1 Parent(s): 94f2291

Update templates/svt.html

Browse files
Files changed (1) hide show
  1. templates/svt.html +434 -412
templates/svt.html CHANGED
@@ -1,444 +1,466 @@
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>
 
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>