Spaces:
Sleeping
Sleeping
| <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> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.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: 100vh; | |
| } | |
| .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; | |
| } | |
| textarea, .form-select { | |
| 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; | |
| } | |
| .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 { | |
| 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); | |
| } | |
| /* Styles de la feuille de dissertation */ | |
| .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; | |
| } | |
| /* Responsive design */ | |
| @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; | |
| } | |
| } | |
| /* Animation d'entrée */ | |
| @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> | |
| </select> | |
| </div> | |
| <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> | |
| <button type="submit" class="primary-button" :disabled="isLoading"> | |
| [[ isLoading ? 'Génération en cours...' : 'Générer la dissertation' ]] | |
| </button> | |
| </form> | |
| </div> | |
| <div v-if="dissertation" class="download-container" data-html2canvas-ignore="true"> | |
| <button class="secondary-button" @click="generatePDF">Télécharger la dissertation 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: [], // Pour stocker la liste des cours | |
| selectedCourse: '', // Pour stocker l'ID du cours sélectionné | |
| isLoading: false, | |
| errorMessage: null, | |
| dissertation: null | |
| } | |
| }, | |
| computed: { | |
| dissertationTypeLabel() { | |
| return this.dissertationType === 'type1' ? 'Type 1' : 'Type 2'; | |
| } | |
| }, | |
| mounted() { | |
| // Au chargement du composant, on récupère la liste des cours | |
| this.fetchCourses(); | |
| }, | |
| methods: { | |
| async fetchCourses() { | |
| try { | |
| const response = await fetch('/api/philosophy/courses'); | |
| if (!response.ok) throw new Error('Impossible de charger les cours. Vérifiez la connexion à la base de données.'); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| this.courses = data; | |
| } catch (error) { | |
| this.errorMessage = error.message; | |
| console.error("Erreur de chargement des cours:", error); | |
| } | |
| }, | |
| async generateDissertation() { | |
| if (!this.question.trim()) { | |
| this.errorMessage = "Veuillez entrer un sujet de dissertation."; | |
| return; | |
| } | |
| this.isLoading = true; | |
| this.errorMessage = null; | |
| this.dissertation = null; | |
| try { | |
| const 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 // On envoie l'ID du cours sélectionné | |
| }) | |
| }); | |
| 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; | |
| } | |
| }, | |
| async generatePDF() { | |
| const element = document.getElementById('dissertation-content'); | |
| if (!element) { | |
| this.errorMessage = "Contenu introuvable pour le PDF."; | |
| return; | |
| } | |
| try { | |
| if (document.fonts && document.fonts.ready) { | |
| await document.fonts.ready; | |
| } | |
| window.scrollTo(0, 0); | |
| const options = { | |
| margin: [15, 15, 15, 15], | |
| filename: 'dissertation-philosophie.pdf', | |
| image: { type: 'jpeg', quality: 0.98 }, | |
| html2canvas: { | |
| scale: 2, | |
| useCORS: true, | |
| logging: true, | |
| backgroundColor: null, | |
| }, | |
| jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } | |
| }; | |
| await html2pdf().set(options).from(element).save(); | |
| } catch (err) { | |
| console.error('Erreur html2pdf:', err); | |
| this.errorMessage = "Erreur lors de la génération du PDF. Voir la console pour les détails."; | |
| } | |
| } | |
| } | |
| }); | |
| // ⚠️ Changer les délimiteurs pour éviter le conflit avec Jinja2 | |
| app.config.compilerOptions.delimiters = ['[[', ']]']; | |
| app.mount('#app'); | |
| </script> | |
| </body> | |
| </html> |