Docfile commited on
Commit
a4fc9a3
·
verified ·
1 Parent(s): b1fdf62

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +487 -231
templates/index.html CHANGED
@@ -1,3 +1,4 @@
 
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
  <head>
@@ -19,6 +20,7 @@
19
  --accent: #6366f1;
20
  --success: #38a169;
21
  --danger: #e53e3e;
 
22
  --background: #f7fafc;
23
  --card-bg: rgba(255, 255, 255, 0.95);
24
  }
@@ -78,27 +80,51 @@
78
  border-color: var(--primary);
79
  }
80
 
81
- .disabled-option {
82
- background: #f9fafb;
83
- color: #9ca3af;
84
- border-color: #e5e7eb;
85
- cursor: not-allowed;
86
- position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
- .coming-soon-badge {
90
- position: absolute;
91
- top: -8px;
92
- right: -8px;
93
- background: #f97316;
94
- color: white;
95
- font-size: 8px;
96
- padding: 2px 6px;
97
- border-radius: 10px;
98
- font-weight: 600;
99
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  /* Animations */
103
  @keyframes fadeIn {
104
  from { opacity: 0; transform: translateY(10px); }
@@ -129,19 +155,19 @@
129
  }
130
 
131
  .backup-content {
132
- display: none;
133
- margin-top: 10px;
134
- max-height: 0;
135
- overflow: hidden;
136
- transition: max-height 0.3s ease, opacity 0.3s ease;
137
- opacity: 0;
138
- }
139
 
140
- .backup-content-expanded {
141
- display: block;
142
- max-height: 2000px;
143
- opacity: 1;
144
- }
145
 
146
  /* Styles for multiple image upload */
147
  .image-preview {
@@ -239,7 +265,7 @@
239
  width: 80px;
240
  height: 20px;
241
  }
242
-
243
  .loader div {
244
  position: absolute;
245
  top: 8px;
@@ -249,37 +275,37 @@
249
  background: var(--primary);
250
  animation-timing-function: cubic-bezier(0, 1, 1, 0);
251
  }
252
-
253
  .loader div:nth-child(1) {
254
  left: 8px;
255
  animation: loader1 0.6s infinite;
256
  }
257
-
258
  .loader div:nth-child(2) {
259
  left: 8px;
260
  animation: loader2 0.6s infinite;
261
  }
262
-
263
  .loader div:nth-child(3) {
264
  left: 32px;
265
  animation: loader2 0.6s infinite;
266
  }
267
-
268
  .loader div:nth-child(4) {
269
  left: 56px;
270
  animation: loader3 0.6s infinite;
271
  }
272
-
273
  @keyframes loader1 {
274
  0% {transform: scale(0);}
275
  100% {transform: scale(1);}
276
  }
277
-
278
  @keyframes loader2 {
279
  0% {transform: translate(0, 0);}
280
  100% {transform: translate(24px, 0);}
281
  }
282
-
283
  @keyframes loader3 {
284
  0% {transform: scale(1);}
285
  100% {transform: scale(0);}
@@ -314,6 +340,9 @@
314
  font-size: 0.95rem;
315
  line-height: 1.6;
316
  }
 
 
 
317
  }
318
  </style>
319
  </head>
@@ -347,12 +376,12 @@
347
  <form id="francais-form" class="space-y-8">
348
  <div class="space-y-3">
349
  <label for="sujet-francais" class="block text-sm font-medium text-gray-700 flex items-center">
350
- <i class="fas fa-book-open mr-2 text-blue-500"></i>Sujet
351
  </label>
352
  <div class="focus-ring">
353
  <textarea id="sujet-francais" name="sujet" rows="4"
354
  class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-blue-500 focus:outline-none transition-all duration-200 resize-none"
355
- placeholder="Entrez votre sujet ici..."></textarea>
356
  </div>
357
  <div class="text-xs text-gray-400 text-right" id="character-count">0 caractères</div>
358
  </div>
@@ -387,11 +416,12 @@
387
  </span>
388
  </label>
389
  <label class="relative">
 
390
  <input type="radio" name="choix" value="dissertation"
391
- class="absolute opacity-0 w-full h-full" disabled>
392
- <span class="flex items-center justify-center p-3 border-2 border-gray-200 rounded-xl text-sm font-medium disabled-option">
393
  Dissertation
394
- <span class="coming-soon-badge">Bientôt</span>
395
  </span>
396
  </label>
397
  </div>
@@ -429,7 +459,7 @@
429
  </div>
430
  </button>
431
  </form>
432
- <div id="francais-output" class="mt-8 p-6 bg-blue-50 rounded-xl prose max-w-none shadow-inner">
433
  <!-- Le contenu généré sera inséré ici -->
434
  </div>
435
  </div>
@@ -447,21 +477,21 @@
447
  <form id="etude-texte-form" class="space-y-8" enctype="multipart/form-data">
448
  <div class="space-y-4">
449
  <label class="block text-sm font-medium text-gray-700 flex items-center">
450
- <i class="fas fa-image mr-2 text-blue-500"></i>Image du texte
451
  </label>
452
  <div class="border-3 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:border-blue-400 transition-all duration-200 group"
453
  id="drop-zone">
454
- <input type="file" id="image-upload" name="images" accept="image/*" class="hidden" multiple>
455
  <div class="space-y-4">
456
  <div class="bg-blue-50 rounded-full w-16 h-16 flex items-center justify-center mx-auto transform transition-transform group-hover:scale-110 group-hover:rotate-6">
457
  <i class="fas fa-cloud-upload-alt text-3xl text-blue-500"></i>
458
  </div>
459
  <p class="text-sm text-gray-600 font-medium">Glissez vos images ici ou cliquez
460
  pour sélectionner</p>
461
- <p class="text-xs text-gray-400">PNG, JPG jusqu'à 10MB</p>
462
  </div>
463
  </div>
464
- <div id="image-preview" class="image-preview"></div>
465
  </div>
466
 
467
  <button type="submit"
@@ -472,7 +502,7 @@
472
  </div>
473
  </button>
474
  </form>
475
- <div id="etude-texte-output" class="mt-8 p-6 bg-blue-50 rounded-xl prose max-w-none shadow-inner">
476
  <!-- Le contenu analysé sera inséré ici -->
477
  </div>
478
  </div>
@@ -513,6 +543,9 @@
513
  </footer>
514
 
515
  <script>
 
 
 
516
  function initializeFileUpload() {
517
  const dropZone = document.getElementById('drop-zone');
518
  const fileInput = document.getElementById('image-upload');
@@ -521,22 +554,15 @@
521
  dropZone.addEventListener('click', () => fileInput.click());
522
 
523
  ['dragenter', 'dragover'].forEach(eventName => {
524
- dropZone.addEventListener(eventName, (e) => {
525
- e.preventDefault();
526
- dropZone.classList.add('border-blue-500', 'bg-blue-50');
527
- });
528
  });
529
 
530
  ['dragleave', 'drop'].forEach(eventName => {
531
- dropZone.addEventListener(eventName, (e) => {
532
- e.preventDefault();
533
- dropZone.classList.remove('border-blue-500', 'bg-blue-50');
534
- });
535
  });
536
 
537
  dropZone.addEventListener('drop', (e) => {
538
  e.preventDefault();
539
- dropZone.classList.remove('border-blue-500', 'bg-blue-50');
540
  const files = e.dataTransfer.files;
541
  handleFiles(files);
542
  });
@@ -544,17 +570,48 @@
544
  fileInput.addEventListener('change', () => {
545
  const files = fileInput.files;
546
  handleFiles(files);
 
 
547
  });
548
 
 
 
 
 
 
 
 
 
 
 
549
  function handleFiles(files) {
 
 
 
 
 
 
550
  for (let i = 0; i < files.length; i++) {
551
  const file = files[i];
552
  if (!file.type.startsWith('image/')) continue;
553
 
554
- const reader = new FileReader();
 
 
 
 
 
 
 
 
 
 
 
 
555
  reader.onload = (e) => {
556
  const previewItem = document.createElement('div');
557
  previewItem.classList.add('image-preview-item', 'scale-in');
 
558
  previewItem.innerHTML = `
559
  <img src="${e.target.result}" alt="${file.name}">
560
  <button class="remove-image" title="Supprimer"><i class="fas fa-times"></i></button>
@@ -563,48 +620,80 @@
563
 
564
  // Remove image event
565
  previewItem.querySelector('.remove-image').addEventListener('click', () => {
566
- previewItem.style.opacity = '0';
567
- previewItem.style.transform = 'scale(0.9)';
568
- setTimeout(() => {
569
- imagePreview.removeChild(previewItem);
570
- }, 300);
571
- });
572
  };
573
  reader.readAsDataURL(file);
574
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  }
576
  }
577
-
578
  function sauvegarderReponse(titre, contenu) {
579
  const sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
580
  const dateSauvegarde = new Date().toISOString();
 
 
 
 
 
581
  sauvegardes.push({
582
- titre,
583
  contenu,
584
  date: dateSauvegarde
585
  });
586
- localStorage.setItem('mariam_ai_sauvegardes', JSON.stringify(sauvegardes));
587
-
588
- // Animation de notification
589
- const notification = document.createElement('div');
590
- notification.className = 'fixed bottom-6 right-6 bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg shadow-lg z-50 flex items-center scale-in';
591
- notification.innerHTML = `
592
- <i class="fas fa-check-circle text-green-500 mr-2"></i>
593
- <span class="text-sm font-medium">Sauvegardé avec succès</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  `;
595
  document.body.appendChild(notification);
596
-
597
- setTimeout(() => {
598
  notification.style.opacity = '0';
599
  notification.style.transform = 'translateY(10px)';
600
  setTimeout(() => {
601
- document.body.removeChild(notification);
 
 
602
  }, 300);
603
  }, 3000);
604
-
605
- afficherSauvegardes();
606
  }
607
 
 
608
  function supprimerSauvegarde(index) {
609
  let sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
610
  sauvegardes.splice(index, 1);
@@ -612,12 +701,14 @@
612
  afficherSauvegardes();
613
  }
614
 
615
-
616
  function afficherSauvegardes() {
617
  const sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
 
 
618
  const backupsList = document.getElementById('backups-list');
619
- backupsList.innerHTML = '';
620
-
621
  if (sauvegardes.length === 0) {
622
  backupsList.innerHTML = `
623
  <div class="bg-blue-50 rounded-lg p-8 text-center">
@@ -640,118 +731,191 @@ function afficherSauvegardes() {
640
  hour: '2-digit',
641
  minute: '2-digit'
642
  });
643
-
 
 
 
644
  const sauvegardeDiv = document.createElement('div');
 
 
 
645
  sauvegardeDiv.className = 'bg-white rounded-lg shadow-md p-4 relative backup-item hover:bg-blue-50 transition-all duration-200';
646
  sauvegardeDiv.innerHTML = `
647
- <div class="flex items-start">
648
- <div class="bg-blue-100 rounded-lg p-2 mr-3">
649
  <i class="fas fa-file-alt text-blue-600"></i>
650
  </div>
651
- <div class="flex-grow">
652
- <h3 class="text-lg font-semibold text-gray-800">${sauvegarde.titre}</h3>
653
  <p class="text-sm text-gray-600 mb-2 flex items-center">
654
  <i class="fas fa-clock text-xs mr-1 text-gray-400"></i>
655
  ${formattedDate}
656
  </p>
657
  </div>
658
- <div class="flex space-x-2">
659
- <button class="text-gray-400 hover:text-blue-600 focus:outline-none copy-btn" data-index="${index}">
660
  <i class="fas fa-copy"></i>
661
  </button>
662
- <button class="text-gray-400 hover:text-red-600 focus:outline-none delete-btn" data-index="${index}">
663
  <i class="fas fa-trash-alt"></i>
664
  </button>
665
  </div>
666
  </div>
667
- <div class="backup-content mt-4 text-sm text-gray-700 prose max-w-none border-t border-gray-100 pt-3" style="display: none;">${marked.parse(sauvegarde.contenu)}</div>
 
 
668
  `;
669
  backupsList.appendChild(sauvegardeDiv);
670
 
671
- // Gestion de l'expansion/contraction du contenu
672
  const backupContentDiv = sauvegardeDiv.querySelector('.backup-content');
673
- sauvegardeDiv.addEventListener('click', (e) => {
674
- // Ignorer les clics sur les boutons
675
- if (e.target.tagName === "BUTTON" || e.target.closest('button')) return;
676
-
 
 
 
677
  // Fermer tous les autres éléments ouverts
678
- document.querySelectorAll('.backup-content').forEach(content => {
679
- if (content !== backupContentDiv && content.style.display === 'block') {
680
- content.style.display = 'none';
681
- }
 
682
  });
683
-
684
- // Basculer l'affichage du contenu actuel
685
- if (backupContentDiv.style.display === 'none') {
686
- backupContentDiv.style.display = 'block';
 
 
687
  setTimeout(() => {
688
- MathJax.typesetPromise([backupContentDiv]);
689
- }, 100);
 
 
690
  } else {
691
- backupContentDiv.style.display = 'none';
 
 
692
  }
693
  });
694
 
695
  // Bouton de copie
696
  const copyButton = sauvegardeDiv.querySelector('.copy-btn');
697
  copyButton.addEventListener('click', (e) => {
698
- e.stopPropagation();
699
  navigator.clipboard.writeText(sauvegarde.contenu).then(() => {
700
- copyButton.querySelector('i').className = 'fas fa-check';
 
701
  setTimeout(() => {
702
- copyButton.querySelector('i').className = 'fas fa-copy';
703
  }, 2000);
 
 
 
704
  });
705
  });
706
 
707
  // Bouton de suppression
708
  const deleteButton = sauvegardeDiv.querySelector('.delete-btn');
709
  deleteButton.addEventListener('click', (e) => {
710
- e.stopPropagation();
711
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  const confirmationModal = document.createElement('div');
713
- confirmationModal.className = 'fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-30 scale-in';
714
  confirmationModal.innerHTML = `
715
  <div class="bg-white rounded-lg p-6 max-w-sm mx-4 shadow-xl">
716
  <div class="flex items-center mb-4">
717
  <div class="bg-red-100 rounded-full p-2 mr-3">
718
  <i class="fas fa-exclamation-triangle text-red-500"></i>
719
  </div>
720
- <h3 class="text-lg font-semibold text-gray-800">Confirmation de suppression</h3>
721
  </div>
722
- <p class="text-gray-600 mb-6">Êtes-vous sûr de vouloir supprimer cette sauvegarde ? Cette action est irréversible.</p>
723
  <div class="flex justify-end space-x-3">
724
- <button class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 cancel-btn">Annuler</button>
725
- <button class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 confirm-btn">Supprimer</button>
726
  </div>
727
  </div>
728
  `;
729
  document.body.appendChild(confirmationModal);
730
-
731
  confirmationModal.querySelector('.cancel-btn').addEventListener('click', () => {
732
- confirmationModal.style.opacity = '0';
733
- setTimeout(() => {
734
- document.body.removeChild(confirmationModal);
735
- }, 300);
736
  });
737
-
738
  confirmationModal.querySelector('.confirm-btn').addEventListener('click', () => {
739
- supprimerSauvegarde(index);
740
- confirmationModal.style.opacity = '0';
741
- setTimeout(() => {
742
- document.body.removeChild(confirmationModal);
743
- }, 300);
744
  });
 
 
 
 
 
 
 
745
  });
746
  });
747
  }
748
 
 
 
 
 
 
 
 
 
 
 
749
  async function submitFrancaisForm() {
750
  const form = document.getElementById('francais-form');
751
  const output = document.getElementById('francais-output');
 
752
 
753
  form.addEventListener('submit', async (e) => {
754
  e.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
  output.innerHTML = `
756
  <div class="flex flex-col items-center justify-center py-8">
757
  <div class="loader mb-4">
@@ -760,7 +924,7 @@ function afficherSauvegardes() {
760
  <div></div>
761
  <div></div>
762
  </div>
763
- <p class="text-sm font-medium text-gray-600">Génération en cours, veuillez patienter...</p>
764
  </div>`;
765
 
766
  const formData = new FormData(form);
@@ -769,29 +933,37 @@ function afficherSauvegardes() {
769
  method: 'POST',
770
  body: formData
771
  });
772
-
 
 
773
  if (!response.ok) {
774
- throw new Error('Erreur réseau');
 
775
  }
776
-
777
- const result = await response.json();
778
-
779
  output.style.opacity = '0';
780
  setTimeout(() => {
781
  output.innerHTML = marked.parse(result.output);
782
  output.style.opacity = '1';
783
-
784
- const sujet = formData.get('sujet');
785
- sauvegarderReponse(sujet, result.output);
786
- MathJax.typesetPromise([output]);
787
- }, 300);
 
 
 
 
 
788
  } catch (error) {
789
- output.innerHTML = `
790
- <div class="flex items-center space-x-3 text-red-500 bg-red-50 p-4 rounded-lg">
791
- <i class="fas fa-exclamation-circle text-xl"></i>
 
792
  <div>
793
- <p class="font-medium">Une erreur est survenue</p>
794
- <p class="text-sm text-red-400 mt-1">Veuillez réessayer ou vérifier votre connexion internet.</p>
795
  </div>
796
  </div>`;
797
  }
@@ -801,23 +973,24 @@ function afficherSauvegardes() {
801
  async function submitEtudeTexteForm() {
802
  const form = document.getElementById('etude-texte-form');
803
  const output = document.getElementById('etude-texte-output');
 
804
 
805
  form.addEventListener('submit', async (e) => {
806
  e.preventDefault();
807
-
808
- const imagePreview = document.getElementById('image-preview');
809
- if (imagePreview.children.length === 0) {
810
- output.innerHTML = `
811
- <div class="flex items-center space-x-3 text-amber-500 bg-amber-50 p-4 rounded-lg">
812
- <i class="fas fa-exclamation-triangle text-xl"></i>
813
- <div>
814
- <p class="font-medium">Aucune image</p>
815
- <p class="text-sm text-amber-400 mt-1">Veuillez ajouter au moins une image pour l'analyse.</p>
816
- </div>
817
- </div>`;
818
- return;
819
  }
820
-
821
  output.innerHTML = `
822
  <div class="flex flex-col items-center justify-center py-8">
823
  <div class="loader mb-4">
@@ -826,41 +999,66 @@ function afficherSauvegardes() {
826
  <div></div>
827
  <div></div>
828
  </div>
829
- <p class="text-sm font-medium text-gray-600">Analyse en cours, veuillez patienter...</p>
830
- <p class="text-xs text-gray-400 mt-2">Ce processus peut prendre quelques instants</p>
831
  </div>`;
832
 
833
- const formData = new FormData(form);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
834
  try {
835
  const response = await fetch('/api/etude-texte', {
836
  method: 'POST',
837
- body: formData
838
  });
839
-
 
 
840
  if (!response.ok) {
841
- throw new Error('Erreur réseau');
 
842
  }
843
-
844
- const result = await response.json();
845
-
846
  output.style.opacity = '0';
847
  setTimeout(() => {
848
  output.innerHTML = marked.parse(result.output);
849
  output.style.opacity = '1';
850
-
 
851
  // Titre par défaut pour les analyses d'images
852
- const titre = "Analyse d'image " + new Date().toLocaleString();
853
  sauvegarderReponse(titre, result.output);
854
- MathJax.typesetPromise([output]);
855
- }, 300);
 
 
 
856
  } catch (error) {
857
- output.innerHTML = `
858
- <div class="flex items-center space-x-3 text-red-500 bg-red-50 p-4 rounded-lg">
859
- <i class="fas fa-exclamation-circle text-xl"></i>
 
860
  <div>
861
- <p class="font-medium">Une erreur est survenue</p>
862
- <p class="text-sm text-red-400 mt-1">Veuillez réessayer ou vérifier votre connexion internet.</p>
863
- </div>
864
  </div>`;
865
  }
866
  });
@@ -869,23 +1067,29 @@ function afficherSauvegardes() {
869
  // Animation des cartes au défilement
870
  function animateOnScroll() {
871
  const cards = document.querySelectorAll('.card-hover');
 
 
 
 
 
 
872
  const observer = new IntersectionObserver((entries) => {
873
- entries.forEach((entry, index) => {
874
  if (entry.isIntersecting) {
875
- setTimeout(() => {
876
- entry.target.style.opacity = '1';
877
- entry.target.style.transform = 'translateY(0)';
878
- }, index * 100);
879
  }
880
  });
881
  }, {
882
- threshold: 0.1
883
  });
884
 
885
  cards.forEach(card => {
886
  card.style.opacity = '0';
887
  card.style.transform = 'translateY(30px)';
888
- card.style.transition = 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)';
 
889
  observer.observe(card);
890
  });
891
  }
@@ -894,32 +1098,53 @@ function afficherSauvegardes() {
894
  function enhanceTextareaFocus() {
895
  const textarea = document.getElementById('sujet-francais');
896
  const counter = document.getElementById('character-count');
897
-
898
  textarea.addEventListener('input', () => {
899
  const length = textarea.value.length;
900
  counter.textContent = `${length} caractère${length !== 1 ? 's' : ''}`;
 
 
901
  });
902
  }
903
 
904
  // Effet de hover pour les boutons radio
905
  function enhanceRadioButtons() {
906
- const radioLabels = document.querySelectorAll('input[type="radio"]:not(:disabled) + span');
907
- radioLabels.forEach(label => {
 
 
 
908
  label.addEventListener('mouseenter', () => {
909
- if (!label.previousElementSibling.checked) {
910
- label.style.borderColor = '#3182ce';
911
- label.style.transform = 'translateY(-2px)';
912
- label.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
913
  }
914
  });
915
-
916
  label.addEventListener('mouseleave', () => {
917
- if (!label.previousElementSibling.checked) {
918
- label.style.borderColor = '#e5e7eb';
919
- label.style.transform = 'translateY(0)';
920
- label.style.boxShadow = 'none';
921
- }
922
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  });
924
  }
925
 
@@ -931,46 +1156,77 @@ function afficherSauvegardes() {
931
  animateOnScroll();
932
  enhanceTextareaFocus();
933
  enhanceRadioButtons();
934
-
935
- // Message de bienvenue avec animation
936
- const welcomeMessage = document.createElement('div');
937
- welcomeMessage.innerHTML = `
938
- <div class="fixed bottom-6 right-6 bg-white rounded-xl shadow-lg p-5 transform transition-all duration-500 opacity-0 translate-y-4 max-w-xs z-50">
939
- <div class="flex items-center space-x-4">
940
- <div class="bg-gradient-to-r from-blue-500 to-blue-700 rounded-full p-3">
941
- <i class="fas fa-robot text-white text-xl"></i>
942
- </div>
943
- <div>
944
- <p class="text-sm font-semibold text-gray-800">Bienvenue sur Mariam AI</p>
945
- <p class="text-xs text-gray-500 mt-1">Votre assistant français intelligent est prêt à vous accompagner.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
946
  </div>
 
 
 
947
  </div>
948
- <button class="absolute top-2 right-2 text-gray-400 hover:text-gray-600 focus:outline-none close-welcome">
949
- <i class="fas fa-times"></i>
950
- </button>
951
- </div>
952
- `;
953
- document.body.appendChild(welcomeMessage);
954
 
955
- setTimeout(() => {
956
- welcomeMessage.firstElementChild.classList.remove('opacity-0', 'translate-y-4');
957
- }, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
958
 
959
- // Bouton de fermeture pour le message de bienvenue
960
- welcomeMessage.querySelector('.close-welcome').addEventListener('click', () => {
961
- welcomeMessage.firstElementChild.classList.add('opacity-0', 'translate-y-4');
962
- setTimeout(() => welcomeMessage.remove(), 500);
963
- });
964
 
965
- setTimeout(() => {
966
- if (document.body.contains(welcomeMessage)) {
967
- welcomeMessage.firstElementChild.classList.add('opacity-0', 'translate-y-4');
968
- setTimeout(() => welcomeMessage.remove(), 500);
969
- }
970
- }, 7000);
 
 
971
 
972
- afficherSauvegardes(); // Appel de la fonction pour afficher les sauvegardes
973
  });
974
  </script>
975
  </body>
976
- </html>
 
1
+
2
  <!DOCTYPE html>
3
  <html lang="fr">
4
  <head>
 
20
  --accent: #6366f1;
21
  --success: #38a169;
22
  --danger: #e53e3e;
23
+ --warning: #f97316; /* Ajout pour messages d'avertissement */
24
  --background: #f7fafc;
25
  --card-bg: rgba(255, 255, 255, 0.95);
26
  }
 
80
  border-color: var(--primary);
81
  }
82
 
83
+ /* Styles pour les messages d'erreur/avertissement */
84
+ .alert-message {
85
+ display: flex;
86
+ align-items: center;
87
+ space-x: 3; /* Ne fonctionnera pas ici, utilisez margin sur l'icône */
88
+ padding: 1rem; /* p-4 */
89
+ border-radius: 0.5rem; /* rounded-lg */
90
+ }
91
+ .alert-message i {
92
+ margin-right: 0.75rem; /* space-x-3 equivalent */
93
+ font-size: 1.25rem; /* text-xl */
94
+ }
95
+ .alert-message div p:first-child {
96
+ font-weight: 500; /* font-medium */
97
+ }
98
+ .alert-message div p:last-child {
99
+ font-size: 0.875rem; /* text-sm */
100
+ margin-top: 0.25rem; /* mt-1 */
101
  }
102
 
103
+ .alert-warning {
104
+ color: #92400e; /* text-amber-700 (approximatif) */
105
+ background-color: #fffbeb; /* bg-amber-50 */
106
+ border: 1px solid #fde68a /* border-amber-200 (approximatif) */
107
+ }
108
+ .alert-warning i {
109
+ color: var(--warning); /* text-amber-500 */
110
+ }
111
+ .alert-warning div p:last-child {
112
+ color: #b45309; /* text-amber-600 (approximatif) */
 
113
  }
114
 
115
+ .alert-danger {
116
+ color: #991b1b; /* text-red-700 (approximatif) */
117
+ background-color: #fef2f2; /* bg-red-50 */
118
+ border: 1px solid #fecaca; /* border-red-200 (approximatif) */
119
+ }
120
+ .alert-danger i {
121
+ color: var(--danger); /* text-red-500 */
122
+ }
123
+ .alert-danger div p:last-child {
124
+ color: #b91c1c; /* text-red-600 (approximatif) */
125
+ }
126
+
127
+
128
  /* Animations */
129
  @keyframes fadeIn {
130
  from { opacity: 0; transform: translateY(10px); }
 
155
  }
156
 
157
  .backup-content {
158
+ display: none;
159
+ margin-top: 10px;
160
+ max-height: 0;
161
+ overflow: hidden;
162
+ transition: max-height 0.3s ease, opacity 0.3s ease;
163
+ opacity: 0;
164
+ }
165
 
166
+ .backup-content-expanded {
167
+ display: block;
168
+ max-height: 2000px; /* Augmenté pour les longs contenus */
169
+ opacity: 1;
170
+ }
171
 
172
  /* Styles for multiple image upload */
173
  .image-preview {
 
265
  width: 80px;
266
  height: 20px;
267
  }
268
+
269
  .loader div {
270
  position: absolute;
271
  top: 8px;
 
275
  background: var(--primary);
276
  animation-timing-function: cubic-bezier(0, 1, 1, 0);
277
  }
278
+
279
  .loader div:nth-child(1) {
280
  left: 8px;
281
  animation: loader1 0.6s infinite;
282
  }
283
+
284
  .loader div:nth-child(2) {
285
  left: 8px;
286
  animation: loader2 0.6s infinite;
287
  }
288
+
289
  .loader div:nth-child(3) {
290
  left: 32px;
291
  animation: loader2 0.6s infinite;
292
  }
293
+
294
  .loader div:nth-child(4) {
295
  left: 56px;
296
  animation: loader3 0.6s infinite;
297
  }
298
+
299
  @keyframes loader1 {
300
  0% {transform: scale(0);}
301
  100% {transform: scale(1);}
302
  }
303
+
304
  @keyframes loader2 {
305
  0% {transform: translate(0, 0);}
306
  100% {transform: translate(24px, 0);}
307
  }
308
+
309
  @keyframes loader3 {
310
  0% {transform: scale(1);}
311
  100% {transform: scale(0);}
 
340
  font-size: 0.95rem;
341
  line-height: 1.6;
342
  }
343
+ .grid-cols-4 { /* Assure que les boutons radio s'affichent bien */
344
+ grid-template-columns: repeat(2, minmax(0, 1fr));
345
+ }
346
  }
347
  </style>
348
  </head>
 
376
  <form id="francais-form" class="space-y-8">
377
  <div class="space-y-3">
378
  <label for="sujet-francais" class="block text-sm font-medium text-gray-700 flex items-center">
379
+ <i class="fas fa-book-open mr-2 text-blue-500"></i>Sujet <span class="text-red-500 ml-1">*</span>
380
  </label>
381
  <div class="focus-ring">
382
  <textarea id="sujet-francais" name="sujet" rows="4"
383
  class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-blue-500 focus:outline-none transition-all duration-200 resize-none"
384
+ placeholder="Entrez votre sujet ici..." required></textarea> <!-- Ajout de required -->
385
  </div>
386
  <div class="text-xs text-gray-400 text-right" id="character-count">0 caractères</div>
387
  </div>
 
416
  </span>
417
  </label>
418
  <label class="relative">
419
+ <!-- MODIFICATION ICI: disabled retiré, span class mise à jour, badge retiré -->
420
  <input type="radio" name="choix" value="dissertation"
421
+ class="custom-radio absolute opacity-0 w-full h-full cursor-pointer">
422
+ <span class="flex items-center justify-center p-3 border-2 border-gray-200 rounded-xl text-sm font-medium transition-all duration-200 hover:border-blue-400">
423
  Dissertation
424
+ <!-- <span class="coming-soon-badge">Bientôt</span> -->
425
  </span>
426
  </label>
427
  </div>
 
459
  </div>
460
  </button>
461
  </form>
462
+ <div id="francais-output" class="mt-8 p-6 bg-blue-50 rounded-xl prose max-w-none shadow-inner min-h-[100px]"> <!-- Ajout de min-h -->
463
  <!-- Le contenu généré sera inséré ici -->
464
  </div>
465
  </div>
 
477
  <form id="etude-texte-form" class="space-y-8" enctype="multipart/form-data">
478
  <div class="space-y-4">
479
  <label class="block text-sm font-medium text-gray-700 flex items-center">
480
+ <i class="fas fa-image mr-2 text-blue-500"></i>Image(s) du texte <span class="text-red-500 ml-1">*</span>
481
  </label>
482
  <div class="border-3 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:border-blue-400 transition-all duration-200 group"
483
  id="drop-zone">
484
+ <input type="file" id="image-upload" name="images" accept="image/*" class="hidden" multiple> <!-- `multiple` permet plusieurs fichiers -->
485
  <div class="space-y-4">
486
  <div class="bg-blue-50 rounded-full w-16 h-16 flex items-center justify-center mx-auto transform transition-transform group-hover:scale-110 group-hover:rotate-6">
487
  <i class="fas fa-cloud-upload-alt text-3xl text-blue-500"></i>
488
  </div>
489
  <p class="text-sm text-gray-600 font-medium">Glissez vos images ici ou cliquez
490
  pour sélectionner</p>
491
+ <p class="text-xs text-gray-400">PNG, JPG, WEBP jusqu'à 10MB</p>
492
  </div>
493
  </div>
494
+ <div id="image-preview" class="image-preview"></div> <!-- Prévisualisation des images -->
495
  </div>
496
 
497
  <button type="submit"
 
502
  </div>
503
  </button>
504
  </form>
505
+ <div id="etude-texte-output" class="mt-8 p-6 bg-blue-50 rounded-xl prose max-w-none shadow-inner min-h-[100px]"> <!-- Ajout de min-h -->
506
  <!-- Le contenu analysé sera inséré ici -->
507
  </div>
508
  </div>
 
543
  </footer>
544
 
545
  <script>
546
+ // Gestionnaire global pour les fichiers ajoutés (évite les doublons)
547
+ const uploadedFiles = new Map();
548
+
549
  function initializeFileUpload() {
550
  const dropZone = document.getElementById('drop-zone');
551
  const fileInput = document.getElementById('image-upload');
 
554
  dropZone.addEventListener('click', () => fileInput.click());
555
 
556
  ['dragenter', 'dragover'].forEach(eventName => {
557
+ dropZone.addEventListener(eventName, highlightDropZone);
 
 
 
558
  });
559
 
560
  ['dragleave', 'drop'].forEach(eventName => {
561
+ dropZone.addEventListener(eventName, unhighlightDropZone);
 
 
 
562
  });
563
 
564
  dropZone.addEventListener('drop', (e) => {
565
  e.preventDefault();
 
566
  const files = e.dataTransfer.files;
567
  handleFiles(files);
568
  });
 
570
  fileInput.addEventListener('change', () => {
571
  const files = fileInput.files;
572
  handleFiles(files);
573
+ // Réinitialiser l'input pour permettre de re-sélectionner le même fichier
574
+ fileInput.value = '';
575
  });
576
 
577
+ function highlightDropZone(e) {
578
+ e.preventDefault();
579
+ dropZone.classList.add('border-blue-500', 'bg-blue-50');
580
+ }
581
+
582
+ function unhighlightDropZone(e) {
583
+ e.preventDefault();
584
+ dropZone.classList.remove('border-blue-500', 'bg-blue-50');
585
+ }
586
+
587
  function handleFiles(files) {
588
+ const currentFiles = fileInput.files; // Nécéssaire pour DataTransferItemList
589
+ const dataTransfer = new DataTransfer();
590
+
591
+ // Ajouter les fichiers déjà présents dans uploadedFiles
592
+ uploadedFiles.forEach(file => dataTransfer.items.add(file));
593
+
594
  for (let i = 0; i < files.length; i++) {
595
  const file = files[i];
596
  if (!file.type.startsWith('image/')) continue;
597
 
598
+ // Eviter les doublons basés sur le nom et la taille
599
+ const fileId = `${file.name}-${file.size}`;
600
+ if (uploadedFiles.has(fileId)) continue;
601
+
602
+ uploadedFiles.set(fileId, file);
603
+ dataTransfer.items.add(file); // Ajouter le nouveau fichier à DataTransfer
604
+ renderPreview(file, fileId);
605
+ }
606
+ fileInput.files = dataTransfer.files; // Mettre à jour l'input file
607
+ }
608
+
609
+ function renderPreview(file, fileId) {
610
+ const reader = new FileReader();
611
  reader.onload = (e) => {
612
  const previewItem = document.createElement('div');
613
  previewItem.classList.add('image-preview-item', 'scale-in');
614
+ previewItem.dataset.fileId = fileId; // Stocker l'ID unique
615
  previewItem.innerHTML = `
616
  <img src="${e.target.result}" alt="${file.name}">
617
  <button class="remove-image" title="Supprimer"><i class="fas fa-times"></i></button>
 
620
 
621
  // Remove image event
622
  previewItem.querySelector('.remove-image').addEventListener('click', () => {
623
+ removeFile(fileId, previewItem);
624
+ });
 
 
 
 
625
  };
626
  reader.readAsDataURL(file);
627
+ }
628
+
629
+ function removeFile(fileId, previewElement) {
630
+ uploadedFiles.delete(fileId); // Retirer du Map
631
+
632
+ // Mettre à jour l'input file
633
+ const dataTransfer = new DataTransfer();
634
+ uploadedFiles.forEach(file => dataTransfer.items.add(file));
635
+ fileInput.files = dataTransfer.files;
636
+
637
+ // Retirer l'aperçu de l'UI
638
+ previewElement.style.opacity = '0';
639
+ previewElement.style.transform = 'scale(0.9)';
640
+ setTimeout(() => {
641
+ imagePreview.removeChild(previewElement);
642
+ }, 300);
643
  }
644
  }
645
+
646
  function sauvegarderReponse(titre, contenu) {
647
  const sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
648
  const dateSauvegarde = new Date().toISOString();
649
+ // Limiter la taille des sauvegardes si nécessaire
650
+ const MAX_SAUVEGARDES = 20;
651
+ if (sauvegardes.length >= MAX_SAUVEGARDES) {
652
+ sauvegardes.shift(); // Supprime la plus ancienne
653
+ }
654
  sauvegardes.push({
655
+ titre: titre || "Sauvegarde sans titre", // Titre par défaut
656
  contenu,
657
  date: dateSauvegarde
658
  });
659
+ try {
660
+ localStorage.setItem('mariam_ai_sauvegardes', JSON.stringify(sauvegardes));
661
+ } catch (e) {
662
+ console.error("Erreur lors de la sauvegarde dans localStorage (peut-être plein):", e);
663
+ // Afficher une erreur à l'utilisateur si localStorage est plein
664
+ displayNotification("Erreur de sauvegarde: Espace de stockage local plein.", "error");
665
+ return; // Arrêter si la sauvegarde échoue
666
+ }
667
+
668
+
669
+ displayNotification("Sauvegardé avec succès", "success");
670
+ afficherSauvegardes(); // Mettre à jour la liste affichée
671
+ }
672
+
673
+ function displayNotification(message, type = 'success') {
674
+ const notification = document.createElement('div');
675
+ const bgColor = type === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700';
676
+ const iconClass = type === 'success' ? 'fa-check-circle text-green-500' : 'fa-exclamation-circle text-red-500';
677
+
678
+ notification.className = `fixed bottom-6 right-6 border ${bgColor} px-4 py-3 rounded-lg shadow-lg z-[100] flex items-center scale-in`; // z-index élevé
679
+ notification.innerHTML = `
680
+ <i class="fas ${iconClass} mr-2"></i>
681
+ <span class="text-sm font-medium">${message}</span>
682
  `;
683
  document.body.appendChild(notification);
684
+
685
+ setTimeout(() => {
686
  notification.style.opacity = '0';
687
  notification.style.transform = 'translateY(10px)';
688
  setTimeout(() => {
689
+ if (document.body.contains(notification)) {
690
+ document.body.removeChild(notification);
691
+ }
692
  }, 300);
693
  }, 3000);
 
 
694
  }
695
 
696
+
697
  function supprimerSauvegarde(index) {
698
  let sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
699
  sauvegardes.splice(index, 1);
 
701
  afficherSauvegardes();
702
  }
703
 
704
+
705
  function afficherSauvegardes() {
706
  const sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
707
+ // Trier par date décroissante (plus récent en premier)
708
+ sauvegardes.sort((a, b) => new Date(b.date) - new Date(a.date));
709
  const backupsList = document.getElementById('backups-list');
710
+ backupsList.innerHTML = ''; // Vider la liste avant de la repeupler
711
+
712
  if (sauvegardes.length === 0) {
713
  backupsList.innerHTML = `
714
  <div class="bg-blue-50 rounded-lg p-8 text-center">
 
731
  hour: '2-digit',
732
  minute: '2-digit'
733
  });
734
+
735
+ // Tronquer le titre si trop long pour l'affichage initial
736
+ const displayTitle = sauvegarde.titre.length > 60 ? sauvegarde.titre.substring(0, 57) + '...' : sauvegarde.titre;
737
+
738
  const sauvegardeDiv = document.createElement('div');
739
+ // Utilise l'index original (avant tri) pour la suppression si nécessaire, ou l'index après tri si la suppression se base sur l'élément affiché.
740
+ // Ici on utilise l'index après tri car on supprime de la liste triée affichée.
741
+ sauvegardeDiv.dataset.index = index; // Utiliser cet index pour la suppression
742
  sauvegardeDiv.className = 'bg-white rounded-lg shadow-md p-4 relative backup-item hover:bg-blue-50 transition-all duration-200';
743
  sauvegardeDiv.innerHTML = `
744
+ <div class="flex items-start cursor-pointer item-header"> <!-- Ajout curseur et classe pour le clic -->
745
+ <div class="bg-blue-100 rounded-lg p-2 mr-3 flex-shrink-0"> <!-- flex-shrink-0 -->
746
  <i class="fas fa-file-alt text-blue-600"></i>
747
  </div>
748
+ <div class="flex-grow overflow-hidden"> <!-- overflow-hidden -->
749
+ <h3 class="text-lg font-semibold text-gray-800 truncate" title="${sauvegarde.titre}">${displayTitle}</h3> <!-- truncate et title -->
750
  <p class="text-sm text-gray-600 mb-2 flex items-center">
751
  <i class="fas fa-clock text-xs mr-1 text-gray-400"></i>
752
  ${formattedDate}
753
  </p>
754
  </div>
755
+ <div class="flex space-x-2 ml-2 flex-shrink-0"> <!-- ml-2 et flex-shrink-0 -->
756
+ <button class="text-gray-400 hover:text-blue-600 focus:outline-none copy-btn p-1" title="Copier">
757
  <i class="fas fa-copy"></i>
758
  </button>
759
+ <button class="text-gray-400 hover:text-red-600 focus:outline-none delete-btn p-1" title="Supprimer">
760
  <i class="fas fa-trash-alt"></i>
761
  </button>
762
  </div>
763
  </div>
764
+ <div class="backup-content mt-4 text-sm text-gray-700 prose max-w-none border-t border-gray-100 pt-3">
765
+ <!-- Contenu chargé dynamiquement -->
766
+ </div>
767
  `;
768
  backupsList.appendChild(sauvegardeDiv);
769
 
770
+ // --- Event Listeners ---
771
  const backupContentDiv = sauvegardeDiv.querySelector('.backup-content');
772
+ const headerDiv = sauvegardeDiv.querySelector('.item-header');
773
+
774
+ // Gestion de l'expansion/contraction du contenu au clic sur l'en-tête
775
+ headerDiv.addEventListener('click', (e) => {
776
+ // Basculer l'affichage du contenu actuel
777
+ const isExpanded = backupContentDiv.classList.contains('backup-content-expanded');
778
+
779
  // Fermer tous les autres éléments ouverts
780
+ document.querySelectorAll('.backup-content-expanded').forEach(content => {
781
+ if (content !== backupContentDiv) {
782
+ content.classList.remove('backup-content-expanded');
783
+ content.innerHTML = ''; // Vider le contenu des autres
784
+ }
785
  });
786
+
787
+ if (!isExpanded) {
788
+ // Charger et afficher le contenu
789
+ backupContentDiv.innerHTML = marked.parse(sauvegarde.contenu);
790
+ backupContentDiv.classList.add('backup-content-expanded');
791
+ // Lancer MathJax seulement après que le contenu soit visible
792
  setTimeout(() => {
793
+ MathJax.typesetPromise([backupContentDiv]).catch(function (err) {
794
+ console.error('MathJax typesetting error:', err);
795
+ });
796
+ }, 50); // petit délai pour s'assurer que l'élément est rendu
797
  } else {
798
+ // Cacher le contenu
799
+ backupContentDiv.classList.remove('backup-content-expanded');
800
+ backupContentDiv.innerHTML = ''; // Vider le contenu quand on ferme
801
  }
802
  });
803
 
804
  // Bouton de copie
805
  const copyButton = sauvegardeDiv.querySelector('.copy-btn');
806
  copyButton.addEventListener('click', (e) => {
807
+ e.stopPropagation(); // Empêche le clic de déclencher l'expansion
808
  navigator.clipboard.writeText(sauvegarde.contenu).then(() => {
809
+ const icon = copyButton.querySelector('i');
810
+ icon.className = 'fas fa-check text-green-500'; // Icône de succès
811
  setTimeout(() => {
812
+ icon.className = 'fas fa-copy'; // Retour à l'icône normale
813
  }, 2000);
814
+ }).catch(err => {
815
+ console.error('Erreur de copie:', err);
816
+ displayNotification("Erreur lors de la copie", "error");
817
  });
818
  });
819
 
820
  // Bouton de suppression
821
  const deleteButton = sauvegardeDiv.querySelector('.delete-btn');
822
  deleteButton.addEventListener('click', (e) => {
823
+ e.stopPropagation(); // Empêche le clic de déclencher l'expansion
824
+ // Utiliser l'index stocké dans le dataset
825
+ const itemIndex = parseInt(sauvegardeDiv.dataset.index, 10);
826
+
827
+ // Chercher l'index réel dans le localStorage (car l'affichage est trié)
828
+ const storedSauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
829
+ storedSauvegardes.sort((a, b) => new Date(b.date) - new Date(a.date)); // Appliquer le même tri
830
+ const itemToDelete = storedSauvegardes[itemIndex];
831
+
832
+ // Retrouver l'index original dans le localStorage non trié
833
+ const originalSauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
834
+ const originalIndex = originalSauvegardes.findIndex(s => s.date === itemToDelete.date && s.titre === itemToDelete.titre);
835
+
836
+
837
+ if (originalIndex === -1) {
838
+ console.error("Impossible de trouver l'élément à supprimer dans le stockage original.");
839
+ displayNotification("Erreur lors de la suppression", "error");
840
+ return;
841
+ }
842
+
843
+
844
+ // Afficher la modale de confirmation
845
  const confirmationModal = document.createElement('div');
846
+ confirmationModal.className = 'fixed inset-0 flex items-center justify-center z-[100] bg-black bg-opacity-30 scale-in'; // z-index élevé
847
  confirmationModal.innerHTML = `
848
  <div class="bg-white rounded-lg p-6 max-w-sm mx-4 shadow-xl">
849
  <div class="flex items-center mb-4">
850
  <div class="bg-red-100 rounded-full p-2 mr-3">
851
  <i class="fas fa-exclamation-triangle text-red-500"></i>
852
  </div>
853
+ <h3 class="text-lg font-semibold text-gray-800">Confirmation</h3>
854
  </div>
855
+ <p class="text-gray-600 mb-6 text-sm">Êtes-vous sûr de vouloir supprimer cette sauvegarde ?</p>
856
  <div class="flex justify-end space-x-3">
857
+ <button class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 cancel-btn text-sm">Annuler</button>
858
+ <button class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 confirm-btn text-sm">Supprimer</button>
859
  </div>
860
  </div>
861
  `;
862
  document.body.appendChild(confirmationModal);
863
+
864
  confirmationModal.querySelector('.cancel-btn').addEventListener('click', () => {
865
+ closeModal(confirmationModal);
 
 
 
866
  });
867
+
868
  confirmationModal.querySelector('.confirm-btn').addEventListener('click', () => {
869
+ supprimerSauvegarde(originalIndex); // Utiliser l'index original pour supprimer
870
+ closeModal(confirmationModal);
 
 
 
871
  });
872
+
873
+ // Fermer la modale en cliquant à l'extérieur
874
+ confirmationModal.addEventListener('click', (event) => {
875
+ if (event.target === confirmationModal) {
876
+ closeModal(confirmationModal);
877
+ }
878
+ });
879
  });
880
  });
881
  }
882
 
883
+ function closeModal(modalElement) {
884
+ modalElement.style.opacity = '0';
885
+ setTimeout(() => {
886
+ if (document.body.contains(modalElement)) {
887
+ document.body.removeChild(modalElement);
888
+ }
889
+ }, 300);
890
+ }
891
+
892
+
893
  async function submitFrancaisForm() {
894
  const form = document.getElementById('francais-form');
895
  const output = document.getElementById('francais-output');
896
+ const sujetTextarea = document.getElementById('sujet-francais'); // Récupérer le textarea
897
 
898
  form.addEventListener('submit', async (e) => {
899
  e.preventDefault();
900
+
901
+ // MODIFICATION: Validation côté client
902
+ const sujetValue = sujetTextarea.value.trim();
903
+ if (!sujetValue) {
904
+ output.innerHTML = `
905
+ <div class="alert-message alert-warning">
906
+ <i class="fas fa-exclamation-triangle"></i>
907
+ <div>
908
+ <p>Champ obligatoire</p>
909
+ <p>Veuillez entrer un sujet avant de générer.</p>
910
+ </div>
911
+ </div>`;
912
+ sujetTextarea.focus(); // Met le focus sur le champ vide
913
+ sujetTextarea.classList.add('border-red-500'); // Ajoute une bordure rouge
914
+ setTimeout(() => sujetTextarea.classList.remove('border-red-500'), 3000); // Retire la bordure après 3s
915
+ return; // Arrête l'exécution si le champ est vide
916
+ }
917
+
918
+
919
  output.innerHTML = `
920
  <div class="flex flex-col items-center justify-center py-8">
921
  <div class="loader mb-4">
 
924
  <div></div>
925
  <div></div>
926
  </div>
927
+ <p class="text-sm font-medium text-gray-600">Génération en cours...</p>
928
  </div>`;
929
 
930
  const formData = new FormData(form);
 
933
  method: 'POST',
934
  body: formData
935
  });
936
+
937
+ const result = await response.json(); // Toujours lire la réponse
938
+
939
  if (!response.ok) {
940
+ // Afficher l'erreur renvoyée par le serveur si disponible
941
+ throw new Error(result.output || `Erreur HTTP ${response.status}`);
942
  }
943
+
944
+
 
945
  output.style.opacity = '0';
946
  setTimeout(() => {
947
  output.innerHTML = marked.parse(result.output);
948
  output.style.opacity = '1';
949
+ output.style.transition = 'opacity 0.3s ease-in-out'; // Transition douce
950
+
951
+ // Utiliser le sujet comme titre, tronqué si nécessaire
952
+ const titreSauvegarde = sujetValue.length > 100 ? sujetValue.substring(0, 97) + '...' : sujetValue;
953
+ sauvegarderReponse(titreSauvegarde, result.output);
954
+ // Lancer MathJax après l'insertion du contenu
955
+ MathJax.typesetPromise([output]).catch(function (err) {
956
+ console.error('MathJax typesetting error:', err);
957
+ });
958
+ }, 100); // Léger délai pour l'animation d'opacité
959
  } catch (error) {
960
+ console.error("Erreur lors de la soumission:", error);
961
+ output.innerHTML = `
962
+ <div class="alert-message alert-danger">
963
+ <i class="fas fa-exclamation-circle"></i>
964
  <div>
965
+ <p>Une erreur est survenue</p>
966
+ <p>${error.message || "Veuillez réessayer ou vérifier votre connexion."}</p>
967
  </div>
968
  </div>`;
969
  }
 
973
  async function submitEtudeTexteForm() {
974
  const form = document.getElementById('etude-texte-form');
975
  const output = document.getElementById('etude-texte-output');
976
+ const fileInput = document.getElementById('image-upload'); // Récupérer l'input file
977
 
978
  form.addEventListener('submit', async (e) => {
979
  e.preventDefault();
980
+
981
+ // Validation: Vérifier si au moins une image est sélectionnée
982
+ if (fileInput.files.length === 0) {
983
+ output.innerHTML = `
984
+ <div class="alert-message alert-warning">
985
+ <i class="fas fa-exclamation-triangle"></i>
986
+ <div>
987
+ <p>Aucune image</p>
988
+ <p>Veuillez ajouter au moins une image pour l'analyse.</p>
989
+ </div>
990
+ </div>`;
991
+ return; // Arrête l'exécution
992
  }
993
+
994
  output.innerHTML = `
995
  <div class="flex flex-col items-center justify-center py-8">
996
  <div class="loader mb-4">
 
999
  <div></div>
1000
  <div></div>
1001
  </div>
1002
+ <p class="text-sm font-medium text-gray-600">Analyse en cours...</p>
1003
+ <p class="text-xs text-gray-400 mt-2">Cela peut prendre un moment</p>
1004
  </div>`;
1005
 
1006
+ const formData = new FormData();
1007
+ // Ajouter uniquement les fichiers valides de notre Map `uploadedFiles`
1008
+ if (uploadedFiles.size === 0) {
1009
+ // Double vérification (normalement impossible si fileInput.files > 0)
1010
+ output.innerHTML = `
1011
+ <div class="alert-message alert-warning">
1012
+ <i class="fas fa-exclamation-triangle"></i>
1013
+ <div>
1014
+ <p>Aucune image valide</p>
1015
+ <p>Veuillez vérifier les images sélectionnées.</p>
1016
+ </div>
1017
+ </div>`;
1018
+ return;
1019
+ }
1020
+ uploadedFiles.forEach((file) => {
1021
+ formData.append('images', file, file.name);
1022
+ });
1023
+
1024
+
1025
  try {
1026
  const response = await fetch('/api/etude-texte', {
1027
  method: 'POST',
1028
+ body: formData // Utiliser notre formData construit
1029
  });
1030
+
1031
+ const result = await response.json(); // Toujours lire la réponse
1032
+
1033
  if (!response.ok) {
1034
+ // Afficher l'erreur renvoyée par le serveur si disponible
1035
+ throw new Error(result.output || `Erreur HTTP ${response.status}`);
1036
  }
1037
+
1038
+
 
1039
  output.style.opacity = '0';
1040
  setTimeout(() => {
1041
  output.innerHTML = marked.parse(result.output);
1042
  output.style.opacity = '1';
1043
+ output.style.transition = 'opacity 0.3s ease-in-out'; // Transition douce
1044
+
1045
  // Titre par défaut pour les analyses d'images
1046
+ const titre = `Analyse d'image(s) - ${new Date().toLocaleDateString('fr-FR')}`;
1047
  sauvegarderReponse(titre, result.output);
1048
+ // Lancer MathJax après l'insertion du contenu
1049
+ MathJax.typesetPromise([output]).catch(function (err) {
1050
+ console.error('MathJax typesetting error:', err);
1051
+ });
1052
+ }, 100); // Léger délai pour l'animation d'opacité
1053
  } catch (error) {
1054
+ console.error("Erreur lors de l'analyse:", error);
1055
+ output.innerHTML = `
1056
+ <div class="alert-message alert-danger">
1057
+ <i class="fas fa-exclamation-circle"></i>
1058
  <div>
1059
+ <p>Une erreur est survenue</p>
1060
+ <p>${error.message || "Veuillez réessayer ou vérifier votre connexion."}</p>
1061
+ </div>
1062
  </div>`;
1063
  }
1064
  });
 
1067
  // Animation des cartes au défilement
1068
  function animateOnScroll() {
1069
  const cards = document.querySelectorAll('.card-hover');
1070
+ // Si IntersectionObserver n'est pas supporté, on affiche directement
1071
+ if (!('IntersectionObserver' in window)) {
1072
+ cards.forEach(card => card.style.opacity = '1');
1073
+ return;
1074
+ }
1075
+
1076
  const observer = new IntersectionObserver((entries) => {
1077
+ entries.forEach((entry) => { // Pas besoin d'index ici si le délai est fixe
1078
  if (entry.isIntersecting) {
1079
+ entry.target.style.opacity = '1';
1080
+ entry.target.style.transform = 'translateY(0)';
1081
+ observer.unobserve(entry.target); // N'observer qu'une fois
 
1082
  }
1083
  });
1084
  }, {
1085
+ threshold: 0.1 // Déclenche quand 10% est visible
1086
  });
1087
 
1088
  cards.forEach(card => {
1089
  card.style.opacity = '0';
1090
  card.style.transform = 'translateY(30px)';
1091
+ // Le style 'scale-in' gère déjà une animation, on ajuste le délai
1092
+ // card.style.transition = 'opacity 0.6s ease-out, transform 0.6s ease-out';
1093
  observer.observe(card);
1094
  });
1095
  }
 
1098
  function enhanceTextareaFocus() {
1099
  const textarea = document.getElementById('sujet-francais');
1100
  const counter = document.getElementById('character-count');
1101
+
1102
  textarea.addEventListener('input', () => {
1103
  const length = textarea.value.length;
1104
  counter.textContent = `${length} caractère${length !== 1 ? 's' : ''}`;
1105
+ // Enlève la bordure d'erreur si l'utilisateur commence à taper
1106
+ textarea.classList.remove('border-red-500');
1107
  });
1108
  }
1109
 
1110
  // Effet de hover pour les boutons radio
1111
  function enhanceRadioButtons() {
1112
+ const radioGroups = document.querySelectorAll('input[type="radio"] + span');
1113
+ radioGroups.forEach(label => {
1114
+ const input = label.previousElementSibling;
1115
+ if (input.disabled) return; // Ne pas appliquer aux boutons désactivés
1116
+
1117
  label.addEventListener('mouseenter', () => {
1118
+ if (!input.checked) {
1119
+ label.classList.add('border-blue-400', 'shadow-md', '-translate-y-0.5');
 
 
1120
  }
1121
  });
1122
+
1123
  label.addEventListener('mouseleave', () => {
1124
+ // Retirer les styles de survol seulement si non coché
1125
+ if (!input.checked) {
1126
+ label.classList.remove('border-blue-400', 'shadow-md', '-translate-y-0.5');
1127
+ }
 
1128
  });
1129
+
1130
+ // Assurer que les styles sont retirés si un autre bouton est coché
1131
+ input.addEventListener('change', () => {
1132
+ // Retrouver tous les labels du même groupe
1133
+ const groupName = input.name;
1134
+ document.querySelectorAll(`input[name="${groupName}"] + span`).forEach(otherLabel => {
1135
+ if (otherLabel !== label) {
1136
+ otherLabel.classList.remove('border-blue-400', 'shadow-md', '-translate-y-0.5');
1137
+ // Rétablir la bordure par défaut si besoin
1138
+ if (!otherLabel.previousElementSibling.checked) {
1139
+ otherLabel.classList.remove('border-primary'); // Si jamais elle était là
1140
+ }
1141
+ } else {
1142
+ // Appliquer les styles du bouton coché (gérés par :checked + span)
1143
+ // et retirer les styles de hover qui pourraient rester
1144
+ label.classList.remove('shadow-md', '-translate-y-0.5');
1145
+ }
1146
+ });
1147
+ });
1148
  });
1149
  }
1150
 
 
1156
  animateOnScroll();
1157
  enhanceTextareaFocus();
1158
  enhanceRadioButtons();
1159
+ afficherSauvegardes(); // Afficher les sauvegardes au chargement
1160
+
1161
+ // Configuration MathJax (peut être mis dans <head> aussi)
1162
+ window.MathJax = {
1163
+ tex: {
1164
+ inlineMath: [['$', '$'], ['\\(', '\\)']],
1165
+ displayMath: [['$$', '$$'], ['\\[', '\\]']]
1166
+ },
1167
+ svg: {
1168
+ fontCache: 'global'
1169
+ },
1170
+ options: {
1171
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'],
1172
+ ignoreHtmlClass: 'tex2jax_ignore',
1173
+ processHtmlClass: 'tex2jax_process'
1174
+ }
1175
+ };
1176
+
1177
+
1178
+ // Message de bienvenue (optionnel)
1179
+ const showWelcome = sessionStorage.getItem('welcomeShown') !== 'true';
1180
+ if (showWelcome) {
1181
+ const welcomeMessageContainer = document.createElement('div');
1182
+ welcomeMessageContainer.innerHTML = `
1183
+ <div class="fixed bottom-6 right-6 bg-white rounded-xl shadow-lg p-5 transform transition-all duration-500 opacity-0 translate-y-4 max-w-xs z-[100]">
1184
+ <div class="flex items-start space-x-4">
1185
+ <div class="bg-gradient-to-r from-blue-500 to-blue-700 rounded-full p-3 mt-1 flex-shrink-0">
1186
+ <i class="fas fa-robot text-white text-xl"></i>
1187
+ </div>
1188
+ <div>
1189
+ <p class="text-sm font-semibold text-gray-800">Bienvenue sur Mariam AI !</p>
1190
+ <p class="text-xs text-gray-500 mt-1">Votre assistant est prêt. Posez un sujet ou analysez un texte.</p>
1191
+ </div>
1192
  </div>
1193
+ <button class="absolute top-2 right-2 text-gray-400 hover:text-gray-600 focus:outline-none close-welcome p-1">
1194
+ <i class="fas fa-times text-xs"></i>
1195
+ </button>
1196
  </div>
1197
+ `;
1198
+ document.body.appendChild(welcomeMessageContainer);
1199
+ const welcomeMessage = welcomeMessageContainer.firstElementChild;
 
 
 
1200
 
1201
+ setTimeout(() => {
1202
+ welcomeMessage.classList.remove('opacity-0', 'translate-y-4');
1203
+ }, 500); // Délai avant apparition
1204
+
1205
+ const closeWelcomeButton = welcomeMessage.querySelector('.close-welcome');
1206
+ closeWelcomeButton.addEventListener('click', () => {
1207
+ closeWelcomeMessage(welcomeMessage);
1208
+ });
1209
+
1210
+ // Fermer automatiquement après un certain temps
1211
+ setTimeout(() => {
1212
+ if (document.body.contains(welcomeMessage)) {
1213
+ closeWelcomeMessage(welcomeMessage);
1214
+ }
1215
+ }, 7000); // 7 secondes
1216
 
1217
+ sessionStorage.setItem('welcomeShown', 'true'); // Marquer comme vu pour cette session
1218
+ }
 
 
 
1219
 
1220
+ function closeWelcomeMessage(element) {
1221
+ element.classList.add('opacity-0', 'translate-y-4');
1222
+ setTimeout(() => {
1223
+ if (element.parentElement) {
1224
+ element.parentElement.remove();
1225
+ }
1226
+ }, 500); // Attend la fin de la transition
1227
+ }
1228
 
 
1229
  });
1230
  </script>
1231
  </body>
1232
+ </html>