Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Assistant de Philosophie (Vue.js)</title> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Kalam&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary-color: #6366f1; /* Indigo moderne */ | |
| --primary-hover: #5b21b6; | |
| --secondary-color: #8b5cf6; /* Violet */ | |
| --accent-color: #06b6d4; /* Cyan */ | |
| --text-primary: #0f172a; /* Slate-900 */ | |
| --text-secondary: #475569; /* Slate-600 */ | |
| --text-muted: #94a3b8; /* Slate-400 */ | |
| --background: #ffffff; | |
| --surface: #f8fafc; /* Slate-50 */ | |
| --border: #e2e8f0; /* Slate-200 */ | |
| --border-focus: #cbd5e1; /* Slate-300 */ | |
| --success: #10b981; /* Emerald-500 */ | |
| --error: #ef4444; /* Red-500 */ | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); | |
| margin: 0; padding: 0; color: var(--text-primary); | |
| line-height: 1.6; font-weight: 400; | |
| -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; | |
| min-height: 10vh; | |
| } | |
| .container { max-width: 900px; margin: 0 auto; padding: 3rem 2rem; } | |
| h1 { font-size: 3.5rem; font-weight: 700; text-align: center; background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin: 0 0 1rem 0; letter-spacing: -0.02em; } | |
| .type-indicator { text-align: center; color: var(--text-muted); font-size: 1.125rem; font-weight: 500; margin-bottom: 3rem; letter-spacing: 0.05em; text-transform: uppercase; } | |
| .form-container { background: var(--background); border-radius: 20px; padding: 2.5rem; margin-bottom: 2rem; border: 1px solid var(--border); backdrop-filter: blur(10px); } | |
| .form-group { margin-bottom: 2rem; } | |
| label { display: block; margin-bottom: 0.75rem; font-weight: 600; color: var(--text-primary); font-size: 0.95rem; letter-spacing: 0.01em; } | |
| /* Style ajouté pour le champ de fichier */ | |
| textarea, .form-select, .file-input { width: 100%; padding: 1rem 1.25rem; border-radius: 12px; border: 2px solid var(--border); font-size: 1rem; line-height: 1.6; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); background-color: var(--background); color: var(--text-primary); font-family: inherit; -webkit-appearance: none; -moz-appearance: none; appearance: none; box-sizing: border-box; } | |
| .file-input { cursor: pointer; } | |
| .file-input:hover { border-color: var(--border-focus); } | |
| .form-select { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236366f1' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); background-position: right 1rem center; background-repeat: no-repeat; background-size: 1.25rem 1.25rem; padding-right: 3rem; cursor: pointer; } | |
| textarea { min-height: 120px; resize: vertical; } | |
| textarea:focus, .form-select:focus, .file-input:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); transform: translateY(-1px); } | |
| .primary-button { display: block; width: 100%; padding: 1.25rem; background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); color: white; border: none; border-radius: 12px; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); letter-spacing: 0.01em; position: relative; overflow: hidden; } | |
| .primary-button::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); transition: left 0.5s; } | |
| .primary-button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(99, 102, 241, 0.3); } | |
| .primary-button:hover:not(:disabled)::before { left: 100%; } | |
| .primary-button:active { transform: translateY(0); } | |
| .primary-button:disabled { background: var(--text-muted); cursor: not-allowed; transform: none; box-shadow: none; } | |
| .download-container { margin-top: 2rem; text-align: center; } | |
| .secondary-button { background: var(--surface); color: var(--text-primary); border: 2px solid var(--border); border-radius: 12px; padding: 1rem 2rem; font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: inline-flex; align-items: center; gap: 0.5rem; } | |
| .secondary-button::before { content: '⬇'; font-size: 1.2rem; } | |
| .secondary-button:hover:not(:disabled) { background: var(--background); border-color: var(--primary-color); color: var(--primary-color); transform: translateY(-1px); box-shadow: 0 5px 15px rgba(99, 102, 241, 0.15); } | |
| .loader { width: 60px; height: 60px; margin: 3rem auto; position: relative; } | |
| .loader::before { content: ''; position: absolute; width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--primary-color), var(--secondary-color), var(--accent-color), var(--primary-color)); animation: spin 1.5s linear infinite; } | |
| .loader::after { content: ''; position: absolute; top: 8px; left: 8px; width: 44px; height: 44px; border-radius: 50%; background: var(--background); } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| .error { color: var(--error); background: rgba(239, 68, 68, 0.1); text-align: center; margin-top: 2rem; padding: 1.25rem; border-radius: 12px; font-weight: 500; border: 1px solid rgba(239, 68, 68, 0.2); } | |
| .dissertation-paper { font-family: 'Kalam', cursive; font-size: 20px; color: #1a2a4c; background-color: #fdfaf4; line-height: 2; background-image: linear-gradient(transparent 97%, #d8e2ee 98%); background-size: 100% 40px; border-left: 3px solid #ffaaab; padding-left: 4em; margin: 2rem 0; padding-top: 30px; padding-bottom: 40px; padding-right: 30px; border-radius: 0 12px 12px 0; -webkit-print-color-adjust: exact; print-color-adjust: exact; } | |
| .dissertation-paper h2 { font-size: 1.5em; text-align: center; margin-bottom: 1.5em; color: #1a2a4c; } | |
| .dissertation-paper h3 { font-size: 1.2em; margin-top: 3em; margin-bottom: 1.5em; text-transform: uppercase; text-decoration: underline; color: #1a2a4c; } | |
| .dissertation-paper .development-block { margin-top: 3em; } | |
| .dissertation-paper p { text-align: justify; margin: 0; padding: 0; } | |
| .dissertation-paper .prof { text-align: center; font-style: italic; margin-bottom: 2em; } | |
| .dissertation-paper .indented { text-indent: 3em; } | |
| .dissertation-paper .transition { margin-top: 2em; margin-bottom: 2em; font-style: italic; color: #4a6a9c; } | |
| .dissertation-paper, .dissertation-paper * { box-sizing: border-box; } | |
| .avoid-page-break { page-break-inside: avoid; break-inside: avoid; } | |
| /* NOUVEAU STYLE POUR LA PRÉVISUALISATION */ | |
| .image-preview-container { | |
| margin-top: 1.5rem; | |
| padding: 1rem; | |
| border: 1px dashed var(--primary-color); | |
| border-radius: 12px; | |
| background-color: var(--surface); | |
| } | |
| .image-preview-container label { | |
| font-size: 0.9rem; | |
| color: var(--primary-color); | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| } | |
| .uploaded-image-preview { | |
| max-width: 100%; | |
| height: auto; | |
| display: block; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.1); | |
| } | |
| /* FIN NOUVEAU STYLE */ | |
| @media (max-width: 768px) { .container { padding: 2rem 1rem; } h1 { font-size: 2.5rem; } .form-container { padding: 1.5rem; } .dissertation-paper { padding-left: 2em; padding-right: 1rem; font-size: 18px; } } | |
| @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } | |
| .container > * { animation: fadeInUp 0.6s ease-out; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app" class="container"> | |
| <h1>Assistant de Dissertation Philosophique</h1> | |
| <p class="type-indicator">Méthodologie : [[ dissertationTypeLabel ]]</p> | |
| <div class="form-container"> | |
| <form @submit.prevent="generateDissertation"> | |
| <div class="form-group"> | |
| <label for="dissertation-type">Choisir la méthodologie</label> | |
| <select id="dissertation-type" v-model="dissertationType" class="form-select"> | |
| <option value="type1">Type 1</option> | |
| <option value="type2">Type 2 (citation)</option> | |
| <option value="type3">Type 3 (commentaire de texte)</option> | |
| </select> | |
| </div> | |
| <!-- AFFICHAGE CONDITIONNEL : Champs pour Type 1 et 2 --> | |
| <div v-if="dissertationType !== 'type3'"> | |
| <div class="form-group"> | |
| <label for="course-context">Choisir un cours comme contexte (optionnel)</label> | |
| <select id="course-context" v-model="selectedCourse" class="form-select"> | |
| <option value="">-- Aucun cours en contexte --</option> | |
| <option v-for="course in courses" :key="course.id" :value="course.id"> | |
| [[ course.title ]] | |
| </option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="question">Entrez votre sujet de dissertation</label> | |
| <textarea id="question" v-model="question" placeholder="Entrez votre sujet ou la citation à analyser ici..."></textarea> | |
| </div> | |
| </div> | |
| <!-- AFFICHAGE CONDITIONNEL : Champ pour Type 3 (Upload d'image) --> | |
| <div v-if="dissertationType === 'type3'" class="form-group"> | |
| <label for="text-image">Téléchargez une image du texte à commenter</label> | |
| <input type="file" @change="handleFileUpload" id="text-image" accept="image/*" class="file-input"> | |
| <!-- PRÉVISUALISATION DE L'IMAGE AJOUTÉE ICI --> | |
| <div v-if="uploadedImageUrl" class="image-preview-container"> | |
| <label>Aperçu du texte à commenter :</label> | |
| <img :src="uploadedImageUrl" alt="Aperçu du texte" class="uploaded-image-preview"> | |
| </div> | |
| </div> | |
| <button type="submit" class="primary-button" :disabled="isLoading"> | |
| [[ isLoading ? 'Génération en cours...' : 'Générer' ]] | |
| </button> | |
| </form> | |
| </div> | |
| <div v-if="dissertation" class="download-container"> | |
| <button class="secondary-button" @click="generatePDF" :disabled="isDownloading"> | |
| [[ isDownloading ? 'Téléchargement...' : 'Télécharger en PDF' ]] | |
| </button> | |
| </div> | |
| <div v-if="isLoading" class="loader"></div> | |
| <p v-if="errorMessage" class="error">[[ errorMessage ]]</p> | |
| <div v-if="dissertation" id="dissertation-content" class="dissertation-paper"> | |
| <h2>Sujet : [[ dissertation.sujet ]]</h2> | |
| <p class="prof">Prof : [[ dissertation.prof ]]</p> | |
| <h3>Introduction</h3> | |
| <p class="indented">[[ dissertation.introduction ]]</p> | |
| <div v-for="partie in dissertation.parties" :key="partie.chapeau" class="avoid-page-break"> | |
| <div class="development-block"> | |
| <p class="indented">[[ partie.chapeau ]]</p> | |
| <p v-for="(arg, idx) in partie.arguments" :key="idx" class="indented"> | |
| [[ arg.paragraphe_argumentatif ]] | |
| </p> | |
| </div> | |
| <p v-if="partie.transition" class="indented transition">[[ partie.transition ]]</p> | |
| </div> | |
| <h3>Conclusion</h3> | |
| <p class="indented">[[ dissertation.conclusion ]]</p> | |
| </div> | |
| </div> | |
| <script> | |
| const { createApp } = Vue; | |
| const app = createApp({ | |
| data() { | |
| return { | |
| question: '', | |
| dissertationType: 'type1', | |
| courses: [], | |
| selectedCourse: '', | |
| uploadedFile: null, | |
| uploadedImageUrl: null, // <-- NOUVEL état pour l'URL de l'image | |
| isLoading: false, | |
| isDownloading: false, | |
| errorMessage: null, | |
| dissertation: null | |
| } | |
| }, | |
| computed: { | |
| dissertationTypeLabel() { | |
| const labels = { | |
| 'type1': 'Type 1', | |
| 'type2': 'Type 2 (Citation)', | |
| 'type3': 'Type 3 (Commentaire de texte)' | |
| }; | |
| return labels[this.dissertationType] || 'Inconnu'; | |
| } | |
| }, | |
| watch: { | |
| // Surveiller le changement de type pour nettoyer la prévisualisation si on quitte le type 3 | |
| dissertationType(newType, oldType) { | |
| if (oldType === 'type3' && newType !== 'type3') { | |
| this.cleanupImage(); | |
| } | |
| } | |
| }, | |
| mounted() { | |
| this.fetchCourses(); | |
| }, | |
| methods: { | |
| // Nouvelle méthode pour nettoyer la mémoire de l'URL objet | |
| cleanupImage() { | |
| if (this.uploadedImageUrl) { | |
| URL.revokeObjectURL(this.uploadedImageUrl); | |
| this.uploadedImageUrl = null; | |
| } | |
| this.uploadedFile = null; | |
| }, | |
| // MÉTHODE MISE À JOUR pour gérer la sélection du fichier et l'aperçu | |
| handleFileUpload(event) { | |
| this.cleanupImage(); // Nettoie l'ancienne image/URL | |
| const file = event.target.files[0]; | |
| this.uploadedFile = file; | |
| this.errorMessage = null; | |
| if (file) { | |
| // Crée une URL locale pour l'aperçu de l'image | |
| this.uploadedImageUrl = URL.createObjectURL(file); | |
| } | |
| }, | |
| async fetchCourses() { | |
| try { | |
| const response = await fetch('/api/philosophy/courses'); | |
| if (!response.ok) throw new Error('Impossible de charger les cours.'); | |
| this.courses = await response.json(); | |
| } catch (error) { | |
| this.errorMessage = error.message; | |
| } | |
| }, | |
| async generateDissertation() { | |
| this.isLoading = true; | |
| this.errorMessage = null; | |
| this.dissertation = null; | |
| try { | |
| let response; | |
| // CAS 1 : C'est un upload d'image (Type 3) | |
| if (this.dissertationType === 'type3') { | |
| if (!this.uploadedFile) { | |
| throw new Error("Veuillez sélectionner une image pour le commentaire de texte."); | |
| } | |
| const formData = new FormData(); | |
| formData.append('image', this.uploadedFile); | |
| formData.append('type', this.dissertationType); | |
| response = await fetch('/api/generate_dissertation', { | |
| method: 'POST', | |
| body: formData, // Le navigateur gère le Content-Type pour FormData | |
| }); | |
| // CAS 2 : C'est une soumission de texte (Type 1 ou 2) | |
| } else { | |
| if (!this.question.trim()) { | |
| throw new Error("Veuillez entrer un sujet de dissertation."); | |
| } | |
| response = await fetch('/api/generate_dissertation', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| question: this.question, | |
| type: this.dissertationType, | |
| courseId: this.selectedCourse | |
| }) | |
| }); | |
| } | |
| // Le traitement de la réponse est commun aux deux cas | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.error || "Une erreur inconnue est survenue."); | |
| } | |
| this.dissertation = data; | |
| } catch (error) { | |
| this.errorMessage = error.message; | |
| } finally { | |
| this.isLoading = false; | |
| // Optionnel : Nettoyer l'image après la soumission réussie | |
| if (this.dissertationType === 'type3') { | |
| // Note: Laisser la prévisualisation peut être utile si l'utilisateur veut resoumettre rapidement | |
| // Si on veut nettoyer l'input file: event.target.value = null; (difficile ici car l'event n'est plus là) | |
| // Pour cet exemple, nous allons laisser la prévisualisation visible jusqu'à la prochaine action. | |
| } | |
| } | |
| }, | |
| async generatePDF() { | |
| if (!this.dissertation) return; | |
| this.isDownloading = true; | |
| this.errorMessage = null; | |
| try { | |
| const response = await fetch('/api/generate_pdf', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(this.dissertation), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || "Erreur serveur lors de la création du PDF."); | |
| } | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'dissertation-philosophie.pdf'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| a.remove(); | |
| } catch (err) { | |
| this.errorMessage = "Erreur lors de la génération du PDF : " + err.message; | |
| } finally { | |
| this.isDownloading = false; | |
| } | |
| } | |
| } | |
| }); | |
| app.config.compilerOptions.delimiters = ['[[', ']]']; | |
| app.mount('#app'); | |
| </script> | |
| </body> | |
| </html> |