Mariam-cc / templates /gestion.html
Docfile's picture
Create gestion.html
0ce9d61 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 Analyses - TextAnalyzer AI</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.13.0/cdn.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
text-align: center;
}
.header h1 {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 1.1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 15px;
padding: 25px;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
color: #667eea;
margin-bottom: 10px;
}
.stat-label {
color: #666;
font-weight: 500;
}
.controls {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.search-box {
flex: 1;
min-width: 300px;
}
.search-box input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e7ff;
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
}
.search-box input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.filter-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-btn {
padding: 10px 20px;
border: none;
border-radius: 25px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
background: #f1f5f9;
color: #64748b;
}
.filter-btn.active {
background: #667eea;
color: white;
}
.refresh-btn {
background: #10b981;
color: white;
border: none;
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.refresh-btn:hover {
background: #059669;
transform: translateY(-2px);
}
.analyses-grid {
display: grid;
gap: 20px;
}
.analysis-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.analysis-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.analysis-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.analysis-info h3 {
font-size: 1.3rem;
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.analysis-meta {
color: #666;
font-size: 0.9rem;
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.analysis-badges {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.badge.deepthink {
background: #fef3c7;
color: #92400e;
}
.badge.standard {
background: #dbeafe;
color: #1d4ed8;
}
.analysis-content {
display: grid;
gap: 20px;
}
.content-section {
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
}
.content-header {
background: #f9fafb;
padding: 15px;
font-weight: 600;
color: #374151;
display: flex;
justify-content: space-between;
align-items: center;
}
.content-body {
padding: 20px;
max-height: 300px;
overflow-y: auto;
line-height: 1.6;
}
.image-section {
text-align: center;
margin-bottom: 20px;
}
.analysis-image {
max-width: 100%;
max-height: 200px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.3s ease;
}
.analysis-image:hover {
transform: scale(1.05);
}
.consignes-section {
background: #f0f9ff;
border-left: 4px solid #0ea5e9;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.consignes-section h4 {
color: #0c4a6e;
margin-bottom: 10px;
}
.toggle-btn {
background: #667eea;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.toggle-btn:hover {
background: #5a67d8;
}
.loading {
text-align: center;
padding: 50px;
color: #666;
}
.spinner {
border: 4px solid #f3f4f6;
border-left: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-data {
text-align: center;
padding: 50px;
color: #666;
}
.error {
background: #fef2f2;
color: #dc2626;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
border-left: 4px solid #dc2626;
}
.back-button {
position: fixed;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
color: #667eea;
border: 2px solid #667eea;
padding: 12px 20px;
border-radius: 25px;
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.back-button:hover {
background: #667eea;
color: white;
transform: translateY(-2px);
}
/* Modal styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 15px;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
position: relative;
}
.modal-close {
position: absolute;
top: 15px;
right: 15px;
background: #dc2626;
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
}
.modal img {
max-width: 100%;
height: auto;
border-radius: 10px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.analysis-header {
flex-direction: column;
align-items: flex-start;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.search-box {
min-width: auto;
}
}
</style>
</head>
<body>
<a href="/" class="back-button">← Retour à l'accueil</a>
<div class="container" x-data="gestionApp()">
<div class="header">
<h1>Gestion des Analyses</h1>
<p>Visualisez et gérez toutes les analyses effectuées par les utilisateurs</p>
</div>
<!-- Statistics -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" x-text="stats.total"></div>
<div class="stat-label">Total Analyses</div>
</div>
<div class="stat-card">
<div class="stat-number" x-text="stats.deepthink"></div>
<div class="stat-label">DeepThink</div>
</div>
<div class="stat-card">
<div class="stat-number" x-text="stats.standard"></div>
<div class="stat-label">Standard</div>
</div>
<div class="stat-card">
<div class="stat-number" x-text="stats.avgTime + 's'"></div>
<div class="stat-label">Temps Moyen</div>
</div>
</div>
<!-- Controls -->
<div class="controls">
<div class="search-box">
<input
type="text"
placeholder="Rechercher par nom d'image, consignes..."
x-model="searchTerm"
@input="filterAnalyses()"
>
</div>
<div class="filter-buttons">
<button
class="filter-btn"
:class="{ 'active': filter === 'all' }"
@click="setFilter('all')"
>
Toutes
</button>
<button
class="filter-btn"
:class="{ 'active': filter === 'deepthink' }"
@click="setFilter('deepthink')"
>
DeepThink
</button>
<button
class="filter-btn"
:class="{ 'active': filter === 'standard' }"
@click="setFilter('standard')"
>
Standard
</button>
</div>
<button class="refresh-btn" @click="loadAnalyses()">
🔄 Actualiser
</button>
</div>
<!-- Error Message -->
<div class="error" x-show="error" x-text="error"></div>
<!-- Loading -->
<div class="loading" x-show="loading">
<div class="spinner"></div>
<p>Chargement des analyses...</p>
</div>
<!-- No Data -->
<div class="no-data" x-show="!loading && filteredAnalyses.length === 0 && !error">
<p>Aucune analyse trouvée</p>
</div>
<!-- Analyses Grid -->
<div class="analyses-grid">
<template x-for="analysis in filteredAnalyses" :key="analysis.id">
<div class="analysis-card">
<div class="analysis-header">
<div class="analysis-info">
<h3 x-text="analysis.image_name || 'Image sans nom'"></h3>
<div class="analysis-meta">
<div class="meta-item">
<span>📅</span>
<span x-text="formatDate(analysis.timestamp)"></span>
</div>
<div class="meta-item">
<span>🌐</span>
<span x-text="analysis.user_ip"></span>
</div>
<div class="meta-item">
<span>⏱️</span>
<span x-text="analysis.processing_time + 's'"></span>
</div>
</div>
</div>
<div class="analysis-badges">
<span
class="badge"
:class="analysis.use_deepthink ? 'deepthink' : 'standard'"
x-text="analysis.use_deepthink ? 'DeepThink' : 'Standard'"
></span>
</div>
</div>
<!-- Image Section -->
<div class="image-section">
<img
:src="`/api/analysis/${analysis.id}/image`"
:alt="analysis.image_name"
class="analysis-image"
@click="showImageModal(analysis.id, analysis.image_name)"
@error="$event.target.style.display='none'"
>
</div>
<!-- Consignes Section -->
<div class="consignes-section" x-show="analysis.consignes">
<h4>Consignes utilisateur :</h4>
<p x-text="analysis.consignes"></p>
</div>
<!-- Content Sections -->
<div class="analysis-content">
<!-- Tableau -->
<div class="content-section">
<div class="content-header">
<span>📊 Tableau d'Analyse</span>
<button
class="toggle-btn"
@click="toggleSection(analysis.id, 'tableau')"
x-text="expandedSections[analysis.id + '_tableau'] ? 'Réduire' : 'Voir'"
></button>
</div>
<div
class="content-body"
x-show="expandedSections[analysis.id + '_tableau']"
x-transition
>
<pre x-text="analysis.tableau_output || 'Aucun contenu'"></pre>
</div>
</div>
<!-- Dissertation -->
<div class="content-section">
<div class="content-header">
<span>✍️ Dissertation</span>
<button
class="toggle-btn"
@click="toggleSection(analysis.id, 'dissertation')"
x-text="expandedSections[analysis.id + '_dissertation'] ? 'Réduire' : 'Voir'"
></button>
</div>
<div
class="content-body"
x-show="expandedSections[analysis.id + '_dissertation']"
x-transition
>
<pre x-text="analysis.dissertation_output || 'Aucun contenu'"></pre>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Image Modal -->
<div class="modal" x-show="showModal" x-transition @click="closeModal()">
<div class="modal-content" @click.stop>
<button class="modal-close" @click="closeModal()">×</button>
<img :src="modalImage" :alt="modalTitle">
</div>
</div>
</div>
<script>
function gestionApp() {
return {
analyses: [],
filteredAnalyses: [],
loading: true,
error: '',
searchTerm: '',
filter: 'all',
expandedSections: {},
showModal: false,
modalImage: '',
modalTitle: '',
stats: {
total: 0,
deepthink: 0,
standard: 0,
avgTime: 0
},
init() {
this.loadAnalyses();
},
async loadAnalyses() {
this.loading = true;
this.error = '';
try {
const response = await fetch('/api/analyses');
if (!response.ok) {
throw new Error('Erreur lors du chargement des données');
}
this.analyses = await response.json();
this.calculateStats();
this.filterAnalyses();
} catch (err) {
this.error = err.message;
console.error('Error loading analyses:', err);
} finally {
this.loading = false;
}
},
calculateStats() {
this.stats.total = this.analyses.length;
this.stats.deepthink = this.analyses.filter(a => a.use_deepthink).length;
this.stats.standard = this.stats.total - this.stats.deepthink;
if (this.stats.total > 0) {
const totalTime = this.analyses.reduce((sum, a) => sum + (a.processing_time || 0), 0);
this.stats.avgTime = Math.round(totalTime / this.stats.total * 100) / 100;
} else {
this.stats.avgTime = 0;
}
},
filterAnalyses() {
let filtered = this.analyses;
// Apply type filter
if (this.filter === 'deepthink') {
filtered = filtered.filter(a => a.use_deepthink);
} else if (this.filter === 'standard') {
filtered = filtered.filter(a => !a.use_deepthink);
}
// Apply search filter
if (this.searchTerm.trim()) {
const term = this.searchTerm.toLowerCase();
filtered = filtered.filter(a =>
(a.image_name && a.image_name.toLowerCase().includes(term)) ||
(a.consignes && a.consignes.toLowerCase().includes(term)) ||
(a.user_ip && a.user_ip.includes(term))
);
}
this.filteredAnalyses = filtered;
},
setFilter(newFilter) {
this.filter = newFilter;
this.filterAnalyses();
},
toggleSection(analysisId, sectionType) {
const key = analysisId + '_' + sectionType;
this.expandedSections[key] = !this.expandedSections[key];
},
showImageModal(analysisId, imageName) {
this.modalImage = `/api/analysis/${analysisId}/image`;
this.modalTitle = imageName;
this.showModal = true;
},
closeModal() {
this.showModal = false;
this.modalImage = '';
this.modalTitle = '';
},
formatDate(dateString) {
return new Date(dateString).toLocaleString('fr-FR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
}
}
</script>
</body>
</html>