Flasksite1 / templates /gestion.html
Docfile's picture
Update templates/gestion.html
455489b verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestion des Dissertations - Mariam AI</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<style>
[x-cloak] { display: none !important; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div x-data="gestionApp()" x-init="loadData()" class="container mx-auto px-4 py-8">
<!-- Header -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-800">Gestion des Dissertations</h1>
<p class="text-gray-600 mt-2">Suivi des requêtes et réponses générées par Mariam AI</p>
</div>
<div class="flex space-x-4">
<a href="/" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition duration-200">
← Retour à l'app
</a>
<button @click="confirmClearAll()" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition duration-200">
Vider tout
</button>
</div>
</div>
</div>
<!-- Statistiques -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white p-6 rounded-lg shadow-md">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-500">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Total</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.total"></p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-500">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Succès</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.success"></p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<div class="flex items-center">
<div class="p-3 rounded-full bg-red-100 text-red-500">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Erreurs</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.errors"></p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-500">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Taux de succès</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.successRate + '%'"></p>
</div>
</div>
</div>
</div>
<!-- Filtres -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<div class="flex flex-wrap gap-4 items-center">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Filtrer par statut</label>
<select x-model="filter.status" @change="applyFilters()" class="border border-gray-300 rounded-md px-3 py-2">
<option value="">Tous</option>
<option value="success">Succès</option>
<option value="error">Erreurs</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Filtrer par type</label>
<select x-model="filter.type" @change="applyFilters()" class="border border-gray-300 rounded-md px-3 py-2">
<option value="">Tous</option>
<option value="type1">Type 1</option>
<option value="type2">Type 2</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Rechercher</label>
<input type="text" x-model="filter.search" @input="applyFilters()"
placeholder="Rechercher dans les sujets..."
class="border border-gray-300 rounded-md px-3 py-2">
</div>
<div class="flex-1"></div>
<button @click="refreshData()" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition duration-200">
🔄 Actualiser
</button>
</div>
</div>
<!-- Messages -->
<div x-show="message.show" x-cloak
:class="message.type === 'success' ? 'bg-green-100 border-green-500 text-green-700' : 'bg-red-100 border-red-500 text-red-700'"
class="border-l-4 p-4 mb-6 rounded">
<p x-text="message.text"></p>
</div>
<!-- Loading -->
<div x-show="loading" x-cloak class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<p class="mt-2 text-gray-600">Chargement...</p>
</div>
<!-- Liste des dissertations -->
<div x-show="!loading" x-cloak class="space-y-6">
<template x-for="item in filteredData" :key="item.id">
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex justify-between items-start mb-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<span class="text-sm text-gray-500" x-text="formatDate(item.timestamp)"></span>
<span :class="item.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
class="px-2 py-1 rounded-full text-xs font-medium"
x-text="item.success ? 'Succès' : 'Erreur'">
</span>
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs font-medium"
x-text="item.input.type">
</span>
</div>
<h3 class="text-lg font-semibold text-gray-800 mb-2" x-text="item.input.question"></h3>
</div>
<button @click="deleteRecord(item.id)"
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm transition duration-200">
Supprimer
</button>
</div>
<!-- Détails de l'input -->
<div class="bg-gray-50 rounded-lg p-4 mb-4">
<h4 class="font-medium text-gray-700 mb-2">Données d'entrée</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span class="font-medium">Question:</span>
<p class="text-gray-600 mt-1" x-text="item.input.question"></p>
</div>
<div>
<span class="font-medium">Type:</span>
<p class="text-gray-600 mt-1" x-text="item.input.type"></p>
</div>
<div>
<span class="font-medium">Cours ID:</span>
<p class="text-gray-600 mt-1" x-text="item.input.courseId || 'Aucun'"></p>
</div>
</div>
</div>
<!-- Résultat ou erreur -->
<div x-show="item.success" x-cloak>
<div class="border-l-4 border-green-500 bg-green-50 p-4 rounded">
<h4 class="font-medium text-green-800 mb-2">Résultat généré</h4>
<div class="space-y-2 text-sm">
<div><strong>Sujet:</strong> <span x-text="item.output?.sujet"></span></div>
<div><strong>Parties:</strong> <span x-text="item.output?.parties?.length + ' partie(s)'"></span></div>
<button @click="toggleDetails(item.id)"
class="text-green-600 hover:text-green-800 underline"
x-text="item.showDetails ? 'Masquer les détails' : 'Voir les détails'">
</button>
<!-- Détails complets -->
<div x-show="item.showDetails" x-cloak class="mt-4 p-4 bg-white rounded border">
<div class="space-y-4 max-h-96 overflow-y-auto">
<div>
<strong class="text-gray-700">Introduction:</strong>
<p class="mt-1 text-gray-600 text-sm" x-text="item.output?.introduction"></p>
</div>
<template x-for="(partie, index) in item.output?.parties" :key="index">
<div class="border-l-2 border-blue-200 pl-4">
<strong class="text-gray-700" x-text="'Partie ' + (index + 1) + ':'"></strong>
<p class="mt-1 text-gray-600 text-sm" x-text="partie.chapeau"></p>
<div class="mt-2">
<template x-for="(arg, argIndex) in partie.arguments" :key="argIndex">
<p class="mt-2 text-gray-600 text-sm pl-4 border-l border-gray-200"
x-text="arg.paragraphe_argumentatif"></p>
</template>
</div>
<p x-show="partie.transition" class="mt-2 text-gray-500 text-sm italic"
x-text="partie.transition"></p>
</div>
</template>
<div>
<strong class="text-gray-700">Conclusion:</strong>
<p class="mt-1 text-gray-600 text-sm" x-text="item.output?.conclusion"></p>
</div>
</div>
</div>
</div>
</div>
</div>
<div x-show="!item.success" x-cloak>
<div class="border-l-4 border-red-500 bg-red-50 p-4 rounded">
<h4 class="font-medium text-red-800 mb-2">Erreur</h4>
<p class="text-red-700 text-sm" x-text="item.error"></p>
</div>
</div>
</div>
</template>
<!-- Message si aucune donnée -->
<div x-show="filteredData.length === 0" x-cloak class="text-center py-12">
<div class="text-gray-400 mb-4">
<svg class="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900">Aucune donnée trouvée</h3>
<p class="text-gray-500">Aucune dissertation n'a encore été générée ou ne correspond aux filtres.</p>
</div>
</div>
<!-- Modal de confirmation -->
<div x-show="showConfirmModal" x-cloak
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3 text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<h3 class="text-lg leading-6 font-medium text-gray-900 mt-2">Confirmer la suppression</h3>
<div class="mt-2 px-7 py-3">
<p class="text-sm text-gray-500">
Êtes-vous sûr de vouloir supprimer toutes les données ? Cette action est irréversible.
</p>
</div>
<div class="flex justify-center space-x-4 mt-4">
<button @click="showConfirmModal = false"
class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400">
Annuler
</button>
<button @click="clearAllData()"
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">
Supprimer tout
</button>
</div>
</div>
</div>
</div>
</div>
<script>
function gestionApp() {
return {
data: [],
filteredData: [],
loading: false,
showConfirmModal: false,
filter: {
status: '',
type: '',
search: ''
},
stats: {
total: 0,
success: 0,
errors: 0,
successRate: 0
},
message: {
show: false,
text: '',
type: 'success'
},
async loadData() {
this.loading = true;
try {
const response = await fetch('/api/gestion/dissertations');
const result = await response.json();
if (result.success) {
this.data = result.data;
this.calculateStats();
this.applyFilters();
} else {
this.showMessage('Erreur lors du chargement des données', 'error');
}
} catch (error) {
console.error('Erreur:', error);
this.showMessage('Erreur de connexion', 'error');
} finally {
this.loading = false;
}
},
async refreshData() {
await this.loadData();
this.showMessage('Données actualisées', 'success');
},
calculateStats() {
this.stats.total = this.data.length;
this.stats.success = this.data.filter(item => item.success).length;
this.stats.errors = this.data.filter(item => !item.success).length;
this.stats.successRate = this.stats.total > 0 ?
Math.round((this.stats.success / this.stats.total) * 100) : 0;
},
applyFilters() {
let filtered = [...this.data];
// Filtre par statut
if (this.filter.status) {
if (this.filter.status === 'success') {
filtered = filtered.filter(item => item.success);
} else if (this.filter.status === 'error') {
filtered = filtered.filter(item => !item.success);
}
}
// Filtre par type
if (this.filter.type) {
filtered = filtered.filter(item => item.input.type === this.filter.type);
}
// Filtre par recherche
if (this.filter.search) {
const searchLower = this.filter.search.toLowerCase();
filtered = filtered.filter(item =>
item.input.question.toLowerCase().includes(searchLower)
);
}
// Trier par date (plus récent d'abord)
filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
this.filteredData = filtered;
},
async deleteRecord(id) {
if (!confirm('Êtes-vous sûr de vouloir supprimer cet enregistrement ?')) {
return;
}
try {
const response = await fetch(`/api/gestion/dissertations/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
await this.loadData();
this.showMessage('Enregistrement supprimé', 'success');
} else {
this.showMessage('Erreur lors de la suppression', 'error');
}
} catch (error) {
console.error('Erreur:', error);
this.showMessage('Erreur de connexion', 'error');
}
},
confirmClearAll() {
this.showConfirmModal = true;
},
async clearAllData() {
this.showConfirmModal = false;
try {
const response = await fetch('/api/gestion/dissertations/clear', {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
await this.loadData();
this.showMessage('Toutes les données ont été supprimées', 'success');
} else {
this.showMessage('Erreur lors de la suppression', 'error');
}
} catch (error) {
console.error('Erreur:', error);
this.showMessage('Erreur de connexion', 'error');
}
},
toggleDetails(id) {
const item = this.data.find(item => item.id === id);
if (item) {
item.showDetails = !item.showDetails;
}
},
formatDate(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString('fr-FR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
},
showMessage(text, type = 'success') {
this.message.text = text;
this.message.type = type;
this.message.show = true;
setTimeout(() => {
this.message.show = false;
}, 3000);
}
}
}
</script>
</body>
</html>