Flasksite / templates /philosophie.html
Docfile's picture
Update templates/philosophie.html
366507d verified
<!DOCTYPE html>
<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>