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> |