Spaces:
Running
Running
Update templates/index.html
Browse files- 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 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
}
|
88 |
|
89 |
-
.
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
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 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
}
|
139 |
|
140 |
-
.backup-content-expanded {
|
141 |
-
|
142 |
-
|
143 |
-
|
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"
|
392 |
-
<span class="flex items-center justify-center p-3 border-2 border-gray-200 rounded-xl text-sm font-medium
|
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 |
-
|
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,
|
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,
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
567 |
-
|
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 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
|
591 |
-
|
592 |
-
|
593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
594 |
`;
|
595 |
document.body.appendChild(notification);
|
596 |
-
|
597 |
-
|
598 |
notification.style.opacity = '0';
|
599 |
notification.style.transform = 'translateY(10px)';
|
600 |
setTimeout(() => {
|
601 |
-
document.body.
|
|
|
|
|
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"
|
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"
|
660 |
<i class="fas fa-copy"></i>
|
661 |
</button>
|
662 |
-
<button class="text-gray-400 hover:text-red-600 focus:outline-none delete-btn"
|
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"
|
|
|
|
|
668 |
`;
|
669 |
backupsList.appendChild(sauvegardeDiv);
|
670 |
|
671 |
-
//
|
672 |
const backupContentDiv = sauvegardeDiv.querySelector('.backup-content');
|
673 |
-
sauvegardeDiv.
|
674 |
-
|
675 |
-
|
676 |
-
|
|
|
|
|
|
|
677 |
// Fermer tous les autres éléments ouverts
|
678 |
-
document.querySelectorAll('.backup-content').forEach(content => {
|
679 |
-
if (content !== backupContentDiv
|
680 |
-
content.
|
681 |
-
|
|
|
682 |
});
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
backupContentDiv.
|
|
|
|
|
687 |
setTimeout(() => {
|
688 |
-
MathJax.typesetPromise([backupContentDiv])
|
689 |
-
|
|
|
|
|
690 |
} else {
|
691 |
-
|
|
|
|
|
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')
|
|
|
701 |
setTimeout(() => {
|
702 |
-
|
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-
|
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
|
721 |
</div>
|
722 |
-
<p class="text-gray-600 mb-6">Êtes-vous sûr de vouloir supprimer cette sauvegarde
|
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
|
733 |
-
setTimeout(() => {
|
734 |
-
document.body.removeChild(confirmationModal);
|
735 |
-
}, 300);
|
736 |
});
|
737 |
-
|
738 |
confirmationModal.querySelector('.confirm-btn').addEventListener('click', () => {
|
739 |
-
supprimerSauvegarde(
|
740 |
-
confirmationModal
|
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
|
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 |
-
|
|
|
775 |
}
|
776 |
-
|
777 |
-
|
778 |
-
|
779 |
output.style.opacity = '0';
|
780 |
setTimeout(() => {
|
781 |
output.innerHTML = marked.parse(result.output);
|
782 |
output.style.opacity = '1';
|
783 |
-
|
784 |
-
|
785 |
-
|
786 |
-
|
787 |
-
|
|
|
|
|
|
|
|
|
|
|
788 |
} catch (error) {
|
789 |
-
|
790 |
-
|
791 |
-
|
|
|
792 |
<div>
|
793 |
-
<p
|
794 |
-
<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 |
-
|
809 |
-
|
810 |
-
|
811 |
-
<div class="
|
812 |
-
|
813 |
-
|
814 |
-
|
815 |
-
|
816 |
-
|
817 |
-
|
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
|
830 |
-
<p class="text-xs text-gray-400 mt-2">
|
831 |
</div>`;
|
832 |
|
833 |
-
const formData = new FormData(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
834 |
try {
|
835 |
const response = await fetch('/api/etude-texte', {
|
836 |
method: 'POST',
|
837 |
-
body: formData
|
838 |
});
|
839 |
-
|
|
|
|
|
840 |
if (!response.ok) {
|
841 |
-
|
|
|
842 |
}
|
843 |
-
|
844 |
-
|
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 =
|
853 |
sauvegarderReponse(titre, result.output);
|
854 |
-
|
855 |
-
|
|
|
|
|
|
|
856 |
} catch (error) {
|
857 |
-
|
858 |
-
|
859 |
-
|
|
|
860 |
<div>
|
861 |
-
<p
|
862 |
-
|
863 |
-
|
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
|
874 |
if (entry.isIntersecting) {
|
875 |
-
|
876 |
-
|
877 |
-
|
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 |
-
|
|
|
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 |
-
|
907 |
-
|
|
|
|
|
|
|
908 |
label.addEventListener('mouseenter', () => {
|
909 |
-
if (!
|
910 |
-
label.
|
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 |
-
|
918 |
-
|
919 |
-
label.
|
920 |
-
|
921 |
-
}
|
922 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
923 |
});
|
924 |
}
|
925 |
|
@@ -931,46 +1156,77 @@ function afficherSauvegardes() {
|
|
931 |
animateOnScroll();
|
932 |
enhanceTextareaFocus();
|
933 |
enhanceRadioButtons();
|
934 |
-
|
935 |
-
|
936 |
-
|
937 |
-
|
938 |
-
|
939 |
-
|
940 |
-
|
941 |
-
|
942 |
-
|
943 |
-
|
944 |
-
|
945 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
946 |
</div>
|
|
|
|
|
|
|
947 |
</div>
|
948 |
-
|
949 |
-
|
950 |
-
|
951 |
-
</div>
|
952 |
-
`;
|
953 |
-
document.body.appendChild(welcomeMessage);
|
954 |
|
955 |
-
|
956 |
-
|
957 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
958 |
|
959 |
-
|
960 |
-
|
961 |
-
welcomeMessage.firstElementChild.classList.add('opacity-0', 'translate-y-4');
|
962 |
-
setTimeout(() => welcomeMessage.remove(), 500);
|
963 |
-
});
|
964 |
|
965 |
-
|
966 |
-
|
967 |
-
|
968 |
-
|
969 |
-
|
970 |
-
|
|
|
|
|
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>
|