Docfile commited on
Commit
8936f52
verified
1 Parent(s): d783c87

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +352 -353
templates/index.html CHANGED
@@ -83,14 +83,17 @@
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 */
@@ -98,30 +101,31 @@
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
 
@@ -159,16 +163,20 @@
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 {
174
  display: flex;
@@ -344,6 +352,82 @@
344
  grid-template-columns: repeat(2, minmax(0, 1fr));
345
  }
346
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  </style>
348
  </head>
349
 
@@ -381,7 +465,7 @@
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,12 +500,10 @@
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>
@@ -451,6 +533,20 @@
451
  </div>
452
  </div>
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  <button type="submit"
455
  class="w-full bg-gradient-to-r from-blue-600 to-blue-800 text-white px-6 py-4 rounded-xl font-medium hover:from-blue-700 hover:to-blue-900 transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg">
456
  <div class="flex items-center justify-center space-x-3">
@@ -459,7 +555,7 @@
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>
@@ -481,7 +577,7 @@
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>
@@ -491,7 +587,7 @@
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,7 +598,7 @@
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>
@@ -638,7 +734,9 @@
638
  previewElement.style.opacity = '0';
639
  previewElement.style.transform = 'scale(0.9)';
640
  setTimeout(() => {
641
- imagePreview.removeChild(previewElement);
 
 
642
  }, 300);
643
  }
644
  }
@@ -646,13 +744,12 @@
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
  });
@@ -660,24 +757,22 @@
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);
@@ -690,7 +785,7 @@
690
  document.body.removeChild(notification);
691
  }
692
  }, 300);
693
- }, 3000);
694
  }
695
 
696
 
@@ -702,203 +797,173 @@
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">
715
- <div class="bg-white rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4 shadow-sm">
716
- <i class="fas fa-folder-open text-blue-300 text-2xl"></i>
717
- </div>
718
- <p class="text-gray-600">Aucune sauvegarde pour le moment.</p>
719
- <p class="text-sm text-gray-500 mt-2">Vos r茅ponses g茅n茅r茅es appara卯tront ici.</p>
720
- </div>
721
- `;
722
- return;
723
- }
724
-
725
- sauvegardes.forEach((sauvegarde, index) => {
726
- const date = new Date(sauvegarde.date);
727
- const formattedDate = date.toLocaleDateString('fr-FR', {
728
- day: 'numeric',
729
- month: 'short',
730
- year: 'numeric',
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 = `
@@ -909,53 +974,56 @@ function closeModal(modalElement) {
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">
922
- <div></div>
923
- <div></div>
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);
 
 
 
931
  try {
932
  const response = await fetch('/api/francais', {
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 = `
@@ -973,13 +1041,12 @@ function closeModal(modalElement) {
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>
@@ -988,68 +1055,52 @@ function closeModal(modalElement) {
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">
997
- <div></div>
998
- <div></div>
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 = `
@@ -1067,29 +1118,27 @@ function closeModal(modalElement) {
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,11 +1147,9 @@ function closeModal(modalElement) {
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
  }
@@ -1112,43 +1159,33 @@ function closeModal(modalElement) {
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
 
1151
- // Initialisation avec tous les effets
1152
  document.addEventListener('DOMContentLoaded', () => {
1153
  initializeFileUpload();
1154
  submitFrancaisForm();
@@ -1156,76 +1193,38 @@ function closeModal(modalElement) {
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>
 
83
  /* Styles pour les messages d'erreur/avertissement */
84
  .alert-message {
85
  display: flex;
86
+ align-items: flex-start; /* Align items start for better text wrapping */
87
+ /* space-x-3 equivalent using margin on icon */
88
  padding: 1rem; /* p-4 */
89
  border-radius: 0.5rem; /* rounded-lg */
90
+ border-width: 1px;
91
+ border-style: solid;
92
  }
93
  .alert-message i {
94
  margin-right: 0.75rem; /* space-x-3 equivalent */
95
  font-size: 1.25rem; /* text-xl */
96
+ margin-top: 0.125rem; /* slight top margin for better alignment */
97
  }
98
  .alert-message div p:first-child {
99
  font-weight: 500; /* font-medium */
 
101
  .alert-message div p:last-child {
102
  font-size: 0.875rem; /* text-sm */
103
  margin-top: 0.25rem; /* mt-1 */
104
+ line-height: 1.4; /* Improved line height for wrap */
105
  }
106
 
107
  .alert-warning {
108
+ color: #92400e; /* text-amber-700 */
109
  background-color: #fffbeb; /* bg-amber-50 */
110
+ border-color: #fde68a /* border-amber-200 */
111
  }
112
  .alert-warning i {
113
  color: var(--warning); /* text-amber-500 */
114
  }
115
  .alert-warning div p:last-child {
116
+ color: #b45309; /* text-amber-600 */
117
  }
118
 
119
  .alert-danger {
120
+ color: #991b1b; /* text-red-700 */
121
  background-color: #fef2f2; /* bg-red-50 */
122
+ border-color: #fecaca; /* border-red-200 */
123
  }
124
  .alert-danger i {
125
  color: var(--danger); /* text-red-500 */
126
  }
127
  .alert-danger div p:last-child {
128
+ color: #b91c1c; /* text-red-600 */
129
  }
130
 
131
 
 
163
  margin-top: 10px;
164
  max-height: 0;
165
  overflow: hidden;
166
+ transition: max-height 0.3s ease-out, opacity 0.3s ease-out, margin-top 0.3s ease-out, padding-top 0.3s ease-out; /* Added padding-top */
167
  opacity: 0;
168
+ padding-top: 0; /* Start with no padding */
169
  }
170
 
171
  .backup-content-expanded {
172
  display: block;
173
+ max-height: 3000px; /* Augment茅 pour les longs contenus */
174
  opacity: 1;
175
+ margin-top: 1rem; /* mt-4 */
176
+ padding-top: 0.75rem; /* pt-3 */
177
  }
178
 
179
+
180
  /* Styles for multiple image upload */
181
  .image-preview {
182
  display: flex;
 
352
  grid-template-columns: repeat(2, minmax(0, 1fr));
353
  }
354
  }
355
+
356
+ /* Style pour la checkbox DeepThink */
357
+ .deepthink-label {
358
+ display: flex;
359
+ align-items: center;
360
+ cursor: pointer;
361
+ font-size: 0.875rem; /* text-sm */
362
+ color: var(--secondary);
363
+ }
364
+ .deepthink-label input[type="checkbox"] {
365
+ appearance: none;
366
+ width: 1.25em;
367
+ height: 1.25em;
368
+ border: 2px solid #cbd5e0; /* border-gray-300 */
369
+ border-radius: 0.375rem; /* rounded-md */
370
+ margin-right: 0.5em;
371
+ position: relative;
372
+ top: 1px;
373
+ transition: all 0.2s ease-in-out;
374
+ }
375
+ .deepthink-label input[type="checkbox"]:checked {
376
+ background-color: var(--primary);
377
+ border-color: var(--primary);
378
+ }
379
+ .deepthink-label input[type="checkbox"]:checked::after {
380
+ content: '\f00c'; /* FontAwesome check icon */
381
+ font-family: 'Font Awesome 6 Free';
382
+ font-weight: 900;
383
+ color: white;
384
+ font-size: 0.8em;
385
+ position: absolute;
386
+ top: 50%;
387
+ left: 50%;
388
+ transform: translate(-50%, -50%);
389
+ }
390
+ .deepthink-label:hover input[type="checkbox"] {
391
+ border-color: var(--primary-dark);
392
+ }
393
+ .deepthink-tooltip {
394
+ position: relative;
395
+ display: inline-block;
396
+ margin-left: 6px;
397
+ }
398
+ .deepthink-tooltip .tooltip-text {
399
+ visibility: hidden;
400
+ width: 220px;
401
+ background-color: #2d3748; /* gray-800 */
402
+ color: #fff;
403
+ text-align: center;
404
+ border-radius: 6px;
405
+ padding: 6px 10px;
406
+ position: absolute;
407
+ z-index: 10;
408
+ bottom: 135%;
409
+ left: 50%;
410
+ margin-left: -110px;
411
+ opacity: 0;
412
+ transition: opacity 0.3s;
413
+ font-size: 0.75rem; /* text-xs */
414
+ line-height: 1.4;
415
+ }
416
+ .deepthink-tooltip .tooltip-text::after {
417
+ content: "";
418
+ position: absolute;
419
+ top: 100%;
420
+ left: 50%;
421
+ margin-left: -5px;
422
+ border-width: 5px;
423
+ border-style: solid;
424
+ border-color: #2d3748 transparent transparent transparent;
425
+ }
426
+ .deepthink-tooltip:hover .tooltip-text {
427
+ visibility: visible;
428
+ opacity: 1;
429
+ }
430
+
431
  </style>
432
  </head>
433
 
 
465
  <div class="focus-ring">
466
  <textarea id="sujet-francais" name="sujet" rows="4"
467
  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"
468
+ placeholder="Entrez votre sujet ici..." required></textarea>
469
  </div>
470
  <div class="text-xs text-gray-400 text-right" id="character-count">0 caract猫res</div>
471
  </div>
 
500
  </span>
501
  </label>
502
  <label class="relative">
 
503
  <input type="radio" name="choix" value="dissertation"
504
  class="custom-radio absolute opacity-0 w-full h-full cursor-pointer">
505
  <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">
506
  Dissertation
 
507
  </span>
508
  </label>
509
  </div>
 
533
  </div>
534
  </div>
535
 
536
+ <!-- MODIFICATION: Ajout de la checkbox DeepThink -->
537
+ <div class="flex items-center justify-start pt-2">
538
+ <label for="deepthink-checkbox" class="deepthink-label">
539
+ <input type="checkbox" id="deepthink-checkbox" name="use_deepthink_visual"> <!-- Name is different to avoid direct form submission -->
540
+ Utiliser DeepThink
541
+ <div class="deepthink-tooltip">
542
+ <i class="fas fa-info-circle text-gray-400"></i>
543
+ <span class="tooltip-text">Utilise un mod猫le plus avanc茅 (Gemini Pro) pour une meilleure qualit茅, mais peut 锚tre plus lent et co没teux.</span>
544
+ </div>
545
+ </label>
546
+ </div>
547
+ <!-- Fin de la modification -->
548
+
549
+
550
  <button type="submit"
551
  class="w-full bg-gradient-to-r from-blue-600 to-blue-800 text-white px-6 py-4 rounded-xl font-medium hover:from-blue-700 hover:to-blue-900 transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98] shadow-lg">
552
  <div class="flex items-center justify-center space-x-3">
 
555
  </div>
556
  </button>
557
  </form>
558
+ <div id="francais-output" class="mt-8 p-6 bg-blue-50 rounded-xl prose max-w-none shadow-inner min-h-[100px]">
559
  <!-- Le contenu g茅n茅r茅 sera ins茅r茅 ici -->
560
  </div>
561
  </div>
 
577
  </label>
578
  <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"
579
  id="drop-zone">
580
+ <input type="file" id="image-upload" name="images" accept="image/*" class="hidden" multiple>
581
  <div class="space-y-4">
582
  <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">
583
  <i class="fas fa-cloud-upload-alt text-3xl text-blue-500"></i>
 
587
  <p class="text-xs text-gray-400">PNG, JPG, WEBP jusqu'脿 10MB</p>
588
  </div>
589
  </div>
590
+ <div id="image-preview" class="image-preview"></div>
591
  </div>
592
 
593
  <button type="submit"
 
598
  </div>
599
  </button>
600
  </form>
601
+ <div id="etude-texte-output" class="mt-8 p-6 bg-blue-50 rounded-xl prose max-w-none shadow-inner min-h-[100px]">
602
  <!-- Le contenu analys茅 sera ins茅r茅 ici -->
603
  </div>
604
  </div>
 
734
  previewElement.style.opacity = '0';
735
  previewElement.style.transform = 'scale(0.9)';
736
  setTimeout(() => {
737
+ if (imagePreview.contains(previewElement)) {
738
+ imagePreview.removeChild(previewElement);
739
+ }
740
  }, 300);
741
  }
742
  }
 
744
  function sauvegarderReponse(titre, contenu) {
745
  const sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
746
  const dateSauvegarde = new Date().toISOString();
 
747
  const MAX_SAUVEGARDES = 20;
748
  if (sauvegardes.length >= MAX_SAUVEGARDES) {
749
+ sauvegardes.shift();
750
  }
751
  sauvegardes.push({
752
+ titre: titre || "Sauvegarde sans titre",
753
  contenu,
754
  date: dateSauvegarde
755
  });
 
757
  localStorage.setItem('mariam_ai_sauvegardes', JSON.stringify(sauvegardes));
758
  } catch (e) {
759
  console.error("Erreur lors de la sauvegarde dans localStorage (peut-锚tre plein):", e);
 
760
  displayNotification("Erreur de sauvegarde: Espace de stockage local plein.", "error");
761
+ return;
762
  }
 
 
763
  displayNotification("Sauvegard茅 avec succ猫s", "success");
764
+ afficherSauvegardes();
765
  }
766
 
767
  function displayNotification(message, type = 'success') {
768
  const notification = document.createElement('div');
769
+ const bgColor = type === 'success' ? 'bg-green-50 border-green-200 text-green-700' : type === 'error' ? 'bg-red-50 border-red-200 text-red-700' : 'bg-yellow-50 border-yellow-200 text-yellow-700';
770
+ const iconClass = type === 'success' ? 'fa-check-circle text-green-500' : type === 'error' ? 'fa-exclamation-circle text-red-500' : 'fa-exclamation-triangle text-yellow-500';
771
 
772
+
773
+ notification.className = `fixed bottom-6 right-6 border ${bgColor} px-4 py-3 rounded-lg shadow-lg z-[100] flex items-center scale-in max-w-sm`; // z-index 茅lev茅, max-width
774
  notification.innerHTML = `
775
+ <i class="fas ${iconClass} mr-3 text-lg flex-shrink-0"></i>
776
  <span class="text-sm font-medium">${message}</span>
777
  `;
778
  document.body.appendChild(notification);
 
785
  document.body.removeChild(notification);
786
  }
787
  }, 300);
788
+ }, type === 'error' ? 6000 : 3000); // Longer display for errors
789
  }
790
 
791
 
 
797
  }
798
 
799
 
800
+ function afficherSauvegardes() {
801
+ const sauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
802
+ sauvegardes.sort((a, b) => new Date(b.date) - new Date(a.date));
803
+ const backupsList = document.getElementById('backups-list');
804
+ backupsList.innerHTML = '';
805
+
806
+ if (sauvegardes.length === 0) {
807
+ backupsList.innerHTML = `
808
+ <div class="bg-blue-50 rounded-lg p-8 text-center">
809
+ <div class="bg-white rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4 shadow-sm">
810
+ <i class="fas fa-folder-open text-blue-300 text-2xl"></i>
811
+ </div>
812
+ <p class="text-gray-600">Aucune sauvegarde pour le moment.</p>
813
+ <p class="text-sm text-gray-500 mt-2">Vos r茅ponses g茅n茅r茅es appara卯tront ici.</p>
814
+ </div>
815
+ `;
816
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
817
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
 
819
+ sauvegardes.forEach((sauvegarde, index) => {
820
+ const date = new Date(sauvegarde.date);
821
+ const formattedDate = date.toLocaleDateString('fr-FR', {
822
+ day: 'numeric',
823
+ month: 'short',
824
+ year: 'numeric',
825
+ hour: '2-digit',
826
+ minute: '2-digit'
827
+ });
828
+ const displayTitle = sauvegarde.titre.length > 60 ? sauvegarde.titre.substring(0, 57) + '...' : sauvegarde.titre;
829
+
830
+ const sauvegardeDiv = document.createElement('div');
831
+ sauvegardeDiv.dataset.index = index;
832
+ sauvegardeDiv.className = 'bg-white rounded-lg shadow-md p-4 relative backup-item hover:bg-blue-50 transition-all duration-200';
833
+ sauvegardeDiv.innerHTML = `
834
+ <div class="flex items-start cursor-pointer item-header">
835
+ <div class="bg-blue-100 rounded-lg p-2 mr-3 flex-shrink-0">
836
+ <i class="fas fa-file-alt text-blue-600"></i>
837
+ </div>
838
+ <div class="flex-grow overflow-hidden">
839
+ <h3 class="text-lg font-semibold text-gray-800 truncate" title="${sauvegarde.titre}">${displayTitle}</h3>
840
+ <p class="text-sm text-gray-600 mb-2 flex items-center">
841
+ <i class="fas fa-clock text-xs mr-1 text-gray-400"></i>
842
+ ${formattedDate}
843
+ </p>
844
+ </div>
845
+ <div class="flex space-x-2 ml-2 flex-shrink-0">
846
+ <button class="text-gray-400 hover:text-blue-600 focus:outline-none copy-btn p-1" title="Copier">
847
+ <i class="fas fa-copy"></i>
848
+ </button>
849
+ <button class="text-gray-400 hover:text-red-600 focus:outline-none delete-btn p-1" title="Supprimer">
850
+ <i class="fas fa-trash-alt"></i>
851
+ </button>
852
+ </div>
853
+ </div>
854
+ <div class="backup-content mt-4 text-sm text-gray-700 prose max-w-none border-t border-gray-100 pt-3">
855
+ <!-- Contenu charg茅 dynamiquement -->
856
+ </div>
857
+ `;
858
+ backupsList.appendChild(sauvegardeDiv);
859
 
860
+ const backupContentDiv = sauvegardeDiv.querySelector('.backup-content');
861
+ const headerDiv = sauvegardeDiv.querySelector('.item-header');
 
862
 
863
+ headerDiv.addEventListener('click', (e) => {
864
+ const isExpanded = backupContentDiv.classList.contains('backup-content-expanded');
865
+ document.querySelectorAll('.backup-content-expanded').forEach(content => {
866
+ if (content !== backupContentDiv) {
867
+ content.classList.remove('backup-content-expanded');
868
+ content.innerHTML = '';
869
+ }
870
+ });
871
+ if (!isExpanded) {
872
+ backupContentDiv.innerHTML = marked.parse(sauvegarde.contenu);
873
+ backupContentDiv.classList.add('backup-content-expanded');
874
+ setTimeout(() => {
875
+ MathJax.typesetPromise([backupContentDiv]).catch(function (err) {
876
+ console.error('MathJax typesetting error:', err);
877
+ });
878
+ }, 50);
879
+ } else {
880
+ backupContentDiv.classList.remove('backup-content-expanded');
881
+ backupContentDiv.innerHTML = '';
882
+ }
883
+ });
884
 
885
+ const copyButton = sauvegardeDiv.querySelector('.copy-btn');
886
+ copyButton.addEventListener('click', (e) => {
887
+ e.stopPropagation();
888
+ navigator.clipboard.writeText(sauvegarde.contenu).then(() => {
889
+ const icon = copyButton.querySelector('i');
890
+ icon.className = 'fas fa-check text-green-500';
891
+ setTimeout(() => {
892
+ icon.className = 'fas fa-copy';
893
+ }, 2000);
894
+ }).catch(err => {
895
+ console.error('Erreur de copie:', err);
896
+ displayNotification("Erreur lors de la copie", "error");
897
+ });
898
+ });
899
 
900
+ const deleteButton = sauvegardeDiv.querySelector('.delete-btn');
901
+ deleteButton.addEventListener('click', (e) => {
902
+ e.stopPropagation();
903
+ const itemIndex = parseInt(sauvegardeDiv.dataset.index, 10);
904
+ const storedSauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
905
+ storedSauvegardes.sort((a, b) => new Date(b.date) - new Date(a.date));
906
+ const itemToDelete = storedSauvegardes[itemIndex];
907
+ const originalSauvegardes = JSON.parse(localStorage.getItem('mariam_ai_sauvegardes') || '[]');
908
+ const originalIndex = originalSauvegardes.findIndex(s => s.date === itemToDelete?.date && s.titre === itemToDelete?.titre); // Added safe navigation
909
+
910
+ if (originalIndex === -1) {
911
+ console.error("Impossible de trouver l'茅l茅ment 脿 supprimer dans le stockage original.");
912
+ displayNotification("Erreur lors de la suppression", "error");
913
+ return;
914
+ }
915
 
916
+ const confirmationModal = document.createElement('div');
917
+ confirmationModal.className = 'fixed inset-0 flex items-center justify-center z-[100] bg-black bg-opacity-30 scale-in';
918
+ confirmationModal.innerHTML = `
919
+ <div class="bg-white rounded-lg p-6 max-w-sm mx-4 shadow-xl">
920
+ <div class="flex items-center mb-4">
921
+ <div class="bg-red-100 rounded-full p-2 mr-3">
922
+ <i class="fas fa-exclamation-triangle text-red-500"></i>
923
+ </div>
924
+ <h3 class="text-lg font-semibold text-gray-800">Confirmation</h3>
925
+ </div>
926
+ <p class="text-gray-600 mb-6 text-sm">脢tes-vous s没r de vouloir supprimer cette sauvegarde ?</p>
927
+ <div class="flex justify-end space-x-3">
928
+ <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>
929
+ <button class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 confirm-btn text-sm">Supprimer</button>
930
+ </div>
931
  </div>
932
+ `;
933
+ document.body.appendChild(confirmationModal);
 
 
 
 
 
 
 
 
934
 
935
+ confirmationModal.querySelector('.cancel-btn').addEventListener('click', () => closeModal(confirmationModal));
936
+ confirmationModal.querySelector('.confirm-btn').addEventListener('click', () => {
937
+ supprimerSauvegarde(originalIndex);
938
+ closeModal(confirmationModal);
939
+ });
940
+ confirmationModal.addEventListener('click', (event) => {
941
+ if (event.target === confirmationModal) closeModal(confirmationModal);
942
+ });
943
+ });
944
  });
945
+ }
946
 
947
+ function closeModal(modalElement) {
948
+ modalElement.style.opacity = '0';
949
+ modalElement.style.transition = 'opacity 0.3s ease';
950
+ setTimeout(() => {
951
+ if (document.body.contains(modalElement)) {
952
+ document.body.removeChild(modalElement);
953
+ }
954
+ }, 300);
955
+ }
 
 
 
 
 
 
 
 
 
956
 
957
 
958
  async function submitFrancaisForm() {
959
  const form = document.getElementById('francais-form');
960
  const output = document.getElementById('francais-output');
961
+ const sujetTextarea = document.getElementById('sujet-francais');
962
+ const deepThinkCheckbox = document.getElementById('deepthink-checkbox'); // MODIFICATION: R茅cup茅rer la checkbox
963
 
964
  form.addEventListener('submit', async (e) => {
965
  e.preventDefault();
966
 
 
967
  const sujetValue = sujetTextarea.value.trim();
968
  if (!sujetValue) {
969
  output.innerHTML = `
 
974
  <p>Veuillez entrer un sujet avant de g茅n茅rer.</p>
975
  </div>
976
  </div>`;
977
+ sujetTextarea.focus();
978
+ sujetTextarea.classList.add('border-red-500');
979
+ setTimeout(() => sujetTextarea.classList.remove('border-red-500'), 3000);
980
+ return;
981
  }
982
 
 
983
  output.innerHTML = `
984
  <div class="flex flex-col items-center justify-center py-8">
985
  <div class="loader mb-4">
986
+ <div></div><div></div><div></div><div></div>
 
 
 
987
  </div>
988
  <p class="text-sm font-medium text-gray-600">G茅n茅ration en cours...</p>
989
+ ${deepThinkCheckbox.checked ? '<p class="text-xs text-blue-500 mt-1">Mode DeepThink activ茅</p>' : ''}
990
  </div>`;
991
 
992
  const formData = new FormData(form);
993
+ // MODIFICATION: Ajouter l'茅tat de la checkbox aux donn茅es envoy茅es
994
+ formData.append('use_deepthink', deepThinkCheckbox.checked);
995
+
996
  try {
997
  const response = await fetch('/api/francais', {
998
  method: 'POST',
999
  body: formData
1000
  });
1001
 
1002
+ const result = await response.json();
1003
 
1004
  if (!response.ok) {
 
1005
  throw new Error(result.output || `Erreur HTTP ${response.status}`);
1006
  }
1007
 
 
1008
  output.style.opacity = '0';
1009
+ output.style.transition = ''; // Remove transition before changing content
1010
+ output.innerHTML = marked.parse(result.output);
1011
+
1012
+ // Re-apply transition for fade-in
1013
+ requestAnimationFrame(() => {
1014
+ output.style.transition = 'opacity 0.3s ease-in-out';
1015
  output.style.opacity = '1';
1016
+ });
1017
+
1018
+
1019
+ const titreSauvegarde = sujetValue.length > 100 ? sujetValue.substring(0, 97) + '...' : sujetValue;
1020
+ const deepThinkSuffix = deepThinkCheckbox.checked ? ' [DeepThink]' : ''; // Ajoute un suffixe au titre si DeepThink est utilis茅
1021
+ sauvegarderReponse(titreSauvegarde + deepThinkSuffix, result.output);
1022
+
1023
+ MathJax.typesetPromise([output]).catch(function (err) {
1024
+ console.error('MathJax typesetting error:', err);
1025
+ });
1026
+
1027
  } catch (error) {
1028
  console.error("Erreur lors de la soumission:", error);
1029
  output.innerHTML = `
 
1041
  async function submitEtudeTexteForm() {
1042
  const form = document.getElementById('etude-texte-form');
1043
  const output = document.getElementById('etude-texte-output');
1044
+ const fileInput = document.getElementById('image-upload');
1045
 
1046
  form.addEventListener('submit', async (e) => {
1047
  e.preventDefault();
1048
 
1049
+ if (uploadedFiles.size === 0) { // Use the size of our managed Map
 
1050
  output.innerHTML = `
1051
  <div class="alert-message alert-warning">
1052
  <i class="fas fa-exclamation-triangle"></i>
 
1055
  <p>Veuillez ajouter au moins une image pour l'analyse.</p>
1056
  </div>
1057
  </div>`;
1058
+ return;
1059
  }
1060
 
1061
  output.innerHTML = `
1062
  <div class="flex flex-col items-center justify-center py-8">
1063
  <div class="loader mb-4">
1064
+ <div></div><div></div><div></div><div></div>
 
 
 
1065
  </div>
1066
  <p class="text-sm font-medium text-gray-600">Analyse en cours...</p>
1067
  <p class="text-xs text-gray-400 mt-2">Cela peut prendre un moment</p>
1068
  </div>`;
1069
 
1070
  const formData = new FormData();
 
 
 
 
 
 
 
 
 
 
 
 
 
1071
  uploadedFiles.forEach((file) => {
1072
  formData.append('images', file, file.name);
1073
  });
1074
 
 
1075
  try {
1076
  const response = await fetch('/api/etude-texte', {
1077
  method: 'POST',
1078
+ body: formData
1079
  });
1080
 
1081
+ const result = await response.json();
1082
 
1083
  if (!response.ok) {
 
1084
  throw new Error(result.output || `Erreur HTTP ${response.status}`);
1085
  }
1086
 
 
1087
  output.style.opacity = '0';
1088
+ output.style.transition = ''; // Remove transition before changing content
1089
+ output.innerHTML = marked.parse(result.output);
1090
+
1091
+ // Re-apply transition for fade-in
1092
+ requestAnimationFrame(() => {
1093
+ output.style.transition = 'opacity 0.3s ease-in-out';
1094
  output.style.opacity = '1';
1095
+ });
1096
+
1097
+ const titre = `Analyse d'image(s) - ${new Date().toLocaleDateString('fr-FR')}`;
1098
+ sauvegarderReponse(titre, result.output);
1099
+
1100
+ MathJax.typesetPromise([output]).catch(function (err) {
1101
+ console.error('MathJax typesetting error:', err);
1102
+ });
1103
+
 
1104
  } catch (error) {
1105
  console.error("Erreur lors de l'analyse:", error);
1106
  output.innerHTML = `
 
1118
  // Animation des cartes au d茅filement
1119
  function animateOnScroll() {
1120
  const cards = document.querySelectorAll('.card-hover');
 
1121
  if (!('IntersectionObserver' in window)) {
1122
  cards.forEach(card => card.style.opacity = '1');
1123
  return;
1124
  }
 
1125
  const observer = new IntersectionObserver((entries) => {
1126
+ entries.forEach((entry) => {
1127
  if (entry.isIntersecting) {
1128
+ // Use requestAnimationFrame for smoother start
1129
+ requestAnimationFrame(() => {
1130
+ entry.target.style.opacity = '1';
1131
+ entry.target.style.transform = 'translateY(0)';
1132
+ });
1133
+ observer.unobserve(entry.target);
1134
  }
1135
  });
1136
+ }, { threshold: 0.1 });
 
 
1137
 
1138
  cards.forEach(card => {
1139
  card.style.opacity = '0';
1140
  card.style.transform = 'translateY(30px)';
1141
+ // Rely on the .scale-in animation defined in CSS for entrance
 
1142
  observer.observe(card);
1143
  });
1144
  }
 
1147
  function enhanceTextareaFocus() {
1148
  const textarea = document.getElementById('sujet-francais');
1149
  const counter = document.getElementById('character-count');
 
1150
  textarea.addEventListener('input', () => {
1151
  const length = textarea.value.length;
1152
  counter.textContent = `${length} caract猫re${length !== 1 ? 's' : ''}`;
 
1153
  textarea.classList.remove('border-red-500');
1154
  });
1155
  }
 
1159
  const radioGroups = document.querySelectorAll('input[type="radio"] + span');
1160
  radioGroups.forEach(label => {
1161
  const input = label.previousElementSibling;
1162
+ if (input.disabled) return;
1163
 
1164
  label.addEventListener('mouseenter', () => {
1165
  if (!input.checked) {
1166
+ // Use Tailwind classes for consistency
1167
+ label.classList.add('border-blue-400', 'shadow-md', 'transform' ,'-translate-y-0.5');
1168
  }
1169
  });
 
1170
  label.addEventListener('mouseleave', () => {
 
1171
  if (!input.checked) {
1172
+ label.classList.remove('border-blue-400', 'shadow-md', 'transform', '-translate-y-0.5');
1173
  }
1174
  });
 
 
1175
  input.addEventListener('change', () => {
 
1176
  const groupName = input.name;
1177
  document.querySelectorAll(`input[name="${groupName}"] + span`).forEach(otherLabel => {
1178
  if (otherLabel !== label) {
1179
+ otherLabel.classList.remove('border-blue-400', 'shadow-md', 'transform', '-translate-y-0.5', 'border-primary'); // Ensure clean state
 
 
 
 
1180
  } else {
1181
+ label.classList.remove('shadow-md', 'transform', '-translate-y-0.5'); // Remove hover effects if checked
 
 
1182
  }
1183
  });
1184
  });
1185
  });
1186
  }
1187
 
1188
+ // Initialisation
1189
  document.addEventListener('DOMContentLoaded', () => {
1190
  initializeFileUpload();
1191
  submitFrancaisForm();
 
1193
  animateOnScroll();
1194
  enhanceTextareaFocus();
1195
  enhanceRadioButtons();
1196
+ afficherSauvegardes();
1197
 
1198
+ // MathJax Config
1199
  window.MathJax = {
1200
+ tex: { inlineMath: [['$', '$'], ['\\(', '\\)']], displayMath: [['$$', '$$'], ['\\[', '\\]']] },
1201
+ svg: { fontCache: 'global' },
1202
+ options: { skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'], ignoreHtmlClass: 'tex2jax_ignore', processHtmlClass: 'tex2jax_process' }
 
 
 
 
 
 
 
 
 
1203
  };
1204
 
1205
+ // Welcome Message
 
1206
  const showWelcome = sessionStorage.getItem('welcomeShown') !== 'true';
1207
  if (showWelcome) {
1208
  const welcomeMessageContainer = document.createElement('div');
1209
  welcomeMessageContainer.innerHTML = `
1210
  <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]">
1211
  <div class="flex items-start space-x-4">
1212
+ <div class="bg-gradient-to-r from-blue-500 to-blue-700 rounded-full p-3 mt-1 flex-shrink-0"> <i class="fas fa-robot text-white text-xl"></i> </div>
1213
+ <div> <p class="text-sm font-semibold text-gray-800">Bienvenue sur Mariam AI !</p> <p class="text-xs text-gray-500 mt-1">Votre assistant est pr锚t. Posez un sujet ou analysez un texte.</p> </div>
 
 
 
 
 
1214
  </div>
1215
+ <button class="absolute top-2 right-2 text-gray-400 hover:text-gray-600 focus:outline-none close-welcome p-1"> <i class="fas fa-times text-xs"></i> </button>
1216
+ </div>`;
 
 
 
1217
  document.body.appendChild(welcomeMessageContainer);
1218
  const welcomeMessage = welcomeMessageContainer.firstElementChild;
1219
+ setTimeout(() => { welcomeMessage.classList.remove('opacity-0', 'translate-y-4'); }, 500);
1220
+ welcomeMessage.querySelector('.close-welcome').addEventListener('click', () => closeWelcomeMessage(welcomeMessage));
1221
+ setTimeout(() => { if (document.body.contains(welcomeMessage)) closeWelcomeMessage(welcomeMessage); }, 7000);
1222
+ sessionStorage.setItem('welcomeShown', 'true');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1223
  }
 
1224
  function closeWelcomeMessage(element) {
1225
  element.classList.add('opacity-0', 'translate-y-4');
1226
+ setTimeout(() => { if (element.parentElement) element.parentElement.remove(); }, 500);
 
 
 
 
1227
  }
 
1228
  });
1229
  </script>
1230
  </body>