Spaces:
Sleeping
Sleeping
| <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 ; } | |
| </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> |