Docfile commited on
Commit
455489b
·
verified ·
1 Parent(s): c7232b6

Update templates/gestion.html

Browse files
Files changed (1) hide show
  1. templates/gestion.html +387 -562
templates/gestion.html CHANGED
@@ -4,619 +4,444 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Gestion des Dissertations - Mariam AI</title>
7
- <script src="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.js"></script>
 
8
  <style>
9
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
10
-
11
- body {
12
- font-family: 'Inter', sans-serif;
13
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
14
- min-height: 100vh;
15
- }
16
-
17
- .glass {
18
- background: rgba(255, 255, 255, 0.1);
19
- backdrop-filter: blur(10px);
20
- border: 1px solid rgba(255, 255, 255, 0.2);
21
- }
22
-
23
- .card-hover {
24
- transition: all 0.3s ease;
25
- }
26
-
27
- .card-hover:hover {
28
- transform: translateY(-2px);
29
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
30
- }
31
-
32
- .table-row:hover {
33
- background: rgba(255, 255, 255, 0.1);
34
- }
35
-
36
- .loading-spinner {
37
- border: 3px solid rgba(255, 255, 255, 0.3);
38
- border-radius: 50%;
39
- border-top: 3px solid white;
40
- width: 20px;
41
- height: 20px;
42
- animation: spin 1s linear infinite;
43
- }
44
-
45
- @keyframes spin {
46
- 0% { transform: rotate(0deg); }
47
- 100% { transform: rotate(360deg); }
48
- }
49
-
50
- .fade-in {
51
- animation: fadeIn 0.5s ease-in-out;
52
- }
53
-
54
- @keyframes fadeIn {
55
- from { opacity: 0; transform: translateY(20px); }
56
- to { opacity: 1; transform: translateY(0); }
57
- }
58
  </style>
59
  </head>
60
- <body class="min-h-screen">
61
- <div class="container mx-auto px-4 py-8">
 
62
  <!-- Header -->
63
- <div class="text-center mb-8">
64
- <h1 class="text-4xl font-bold text-white mb-4">🎓 Gestion des Dissertations</h1>
65
- <p class="text-white/80 text-lg">Tableau de bord administrateur - Mariam AI</p>
 
 
 
 
 
 
 
 
 
 
 
 
66
  </div>
67
 
68
- <!-- Stats Cards -->
69
  <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
70
- <div class="glass rounded-xl p-6 text-center card-hover">
71
- <div class="text-3xl font-bold text-white" id="totalDissertations">-</div>
72
- <div class="text-white/80">Total Dissertations</div>
 
 
 
 
 
 
 
 
 
73
  </div>
74
- <div class="glass rounded-xl p-6 text-center card-hover">
75
- <div class="text-3xl font-bold text-white" id="todayCount">-</div>
76
- <div class="text-white/80">Aujourd'hui</div>
 
 
 
 
 
 
 
 
 
 
77
  </div>
78
- <div class="glass rounded-xl p-6 text-center card-hover">
79
- <div class="text-3xl font-bold text-white" id="type1Count">-</div>
80
- <div class="text-white/80">Type 1</div>
 
 
 
 
 
 
 
 
 
 
81
  </div>
82
- <div class="glass rounded-xl p-6 text-center card-hover">
83
- <div class="text-3xl font-bold text-white" id="type2Count">-</div>
84
- <div class="text-white/80">Type 2</div>
 
 
 
 
 
 
 
 
 
 
85
  </div>
86
  </div>
87
 
88
- <!-- Search and Filters -->
89
- <div class="glass rounded-xl p-6 mb-6">
90
- <div class="flex flex-col md:flex-row gap-4 mb-4">
91
- <div class="flex-1">
92
- <input type="text"
93
- id="searchInput"
94
- placeholder="Rechercher par question ou cours..."
95
- class="w-full px-4 py-2 rounded-lg bg-white/20 border border-white/30 text-white placeholder-white/60 focus:outline-none focus:border-white/60">
 
 
96
  </div>
97
  <div>
98
- <select id="typeFilter" class="px-4 py-2 rounded-lg bg-white/20 border border-white/30 text-white focus:outline-none">
99
- <option value="">Tous les types</option>
 
100
  <option value="type1">Type 1</option>
101
  <option value="type2">Type 2</option>
102
  </select>
103
  </div>
104
  <div>
105
- <button id="searchBtn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
106
- 🔍 Rechercher
107
- </button>
 
108
  </div>
 
 
 
 
109
  </div>
110
  </div>
111
 
112
- <!-- Dissertations Table -->
113
- <div class="glass rounded-xl overflow-hidden">
114
- <div class="p-6 border-b border-white/20">
115
- <h2 class="text-xl font-semibold text-white">📚 Liste des Dissertations</h2>
116
- </div>
117
-
118
- <div id="loadingIndicator" class="p-8 text-center">
119
- <div class="loading-spinner mx-auto mb-4"></div>
120
- <p class="text-white/80">Chargement des données...</p>
121
- </div>
122
-
123
- <div id="dissertationsContainer" class="hidden">
124
- <div class="overflow-x-auto">
125
- <table class="w-full">
126
- <thead class="bg-white/10">
127
- <tr class="text-white/90 text-left">
128
- <th class="p-4 font-medium">Date</th>
129
- <th class="p-4 font-medium">Question</th>
130
- <th class="p-4 font-medium">Type</th>
131
- <th class="p-4 font-medium">Cours</th>
132
- <th class="p-4 font-medium">IP Utilisateur</th>
133
- <th class="p-4 font-medium">Actions</th>
134
- </tr>
135
- </thead>
136
- <tbody id="dissertationsTable" class="text-white/80">
137
- <!-- Les lignes seront ajoutées dynamiquement -->
138
- </tbody>
139
- </table>
140
- </div>
141
-
142
- <!-- Pagination -->
143
- <div id="paginationContainer" class="p-4 border-t border-white/20 flex justify-between items-center">
144
- <div class="text-white/80">
145
- <span id="paginationInfo">Affichage de 0 à 0 sur 0 résultats</span>
146
- </div>
147
- <div class="flex gap-2">
148
- <button id="prevBtn" class="px-4 py-2 bg-white/20 hover:bg-white/30 text-white rounded-lg transition-colors disabled:opacity-50">
149
- ← Précédent
150
- </button>
151
- <span id="pageNumbers" class="flex gap-1"></span>
152
- <button id="nextBtn" class="px-4 py-2 bg-white/20 hover:bg-white/30 text-white rounded-lg transition-colors disabled:opacity-50">
153
- Suivant →
154
- </button>
155
- </div>
156
- </div>
157
- </div>
158
  </div>
159
- </div>
160
 
161
- <!-- Modal de détail -->
162
- <div id="detailModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
163
- <div class="glass rounded-xl max-w-4xl max-h-full overflow-y-auto">
164
- <div class="p-6 border-b border-white/20 flex justify-between items-center">
165
- <h3 class="text-xl font-semibold text-white">📄 Détail de la Dissertation</h3>
166
- <button id="closeModal" class="text-white/60 hover:text-white text-2xl">×</button>
167
- </div>
168
- <div id="modalContent" class="p-6 text-white">
169
- <!-- Contenu du modal ajouté dynamiquement -->
170
- </div>
171
  </div>
172
- </div>
173
 
174
- <!-- Modal de confirmation de suppression -->
175
- <div id="deleteModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
176
- <div class="glass rounded-xl p-6 max-w-md">
177
- <h3 class="text-lg font-semibold text-white mb-4">⚠️ Confirmer la suppression</h3>
178
- <p class="text-white/80 mb-6">Êtes-vous sûr de vouloir supprimer cette dissertation ? Cette action est irréversible.</p>
179
- <div class="flex gap-4 justify-end">
180
- <button id="cancelDelete" class="px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg transition-colors">
181
- Annuler
182
- </button>
183
- <button id="confirmDelete" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
184
- Supprimer
185
- </button>
186
- </div>
187
- </div>
188
- </div>
189
-
190
- <script>
191
- let currentPage = 1;
192
- let currentSearch = '';
193
- let currentType = '';
194
- let deleteId = null;
195
-
196
- // Initialisation
197
- document.addEventListener('DOMContentLoaded', function() {
198
- loadDissertations();
199
- setupEventListeners();
200
- });
201
-
202
- function setupEventListeners() {
203
- // Recherche
204
- document.getElementById('searchBtn').addEventListener('click', performSearch);
205
- document.getElementById('searchInput').addEventListener('keypress', function(e) {
206
- if (e.key === 'Enter') performSearch();
207
- });
208
-
209
- // Modal handlers
210
- document.getElementById('closeModal').addEventListener('click', closeModal);
211
- document.getElementById('cancelDelete').addEventListener('click', closeDeleteModal);
212
- document.getElementById('confirmDelete').addEventListener('click', confirmDelete);
213
-
214
- // Click outside modal to close
215
- document.getElementById('detailModal').addEventListener('click', function(e) {
216
- if (e.target === this) closeModal();
217
- });
218
- document.getElementById('deleteModal').addEventListener('click', function(e) {
219
- if (e.target === this) closeDeleteModal();
220
- });
221
- }
222
-
223
- function performSearch() {
224
- currentSearch = document.getElementById('searchInput').value.trim();
225
- currentType = document.getElementById('typeFilter').value;
226
- currentPage = 1;
227
- loadDissertations();
228
- }
229
-
230
- async function loadDissertations() {
231
- try {
232
- showLoading();
233
-
234
- const params = new URLSearchParams({
235
- page: currentPage,
236
- per_page: 10,
237
- search: currentSearch,
238
- type: currentType
239
- });
240
-
241
- const response = await fetch(`/api/dissertations?${params}`);
242
- const data = await response.json();
243
-
244
- if (response.ok) {
245
- displayDissertations(data);
246
- updateStats(data);
247
- } else {
248
- throw new Error(data.error || 'Erreur lors du chargement');
249
- }
250
- } catch (error) {
251
- console.error('Erreur:', error);
252
- showError('Erreur lors du chargement des données');
253
- } finally {
254
- hideLoading();
255
- }
256
- }
257
-
258
- function showLoading() {
259
- document.getElementById('loadingIndicator').classList.remove('hidden');
260
- document.getElementById('dissertationsContainer').classList.add('hidden');
261
- }
262
-
263
- function hideLoading() {
264
- document.getElementById('loadingIndicator').classList.add('hidden');
265
- document.getElementById('dissertationsContainer').classList.remove('hidden');
266
- }
267
-
268
- function displayDissertations(data) {
269
- const tbody = document.getElementById('dissertationsTable');
270
- tbody.innerHTML = '';
271
-
272
- data.dissertations.forEach(dissertation => {
273
- const row = createDissertationRow(dissertation);
274
- tbody.appendChild(row);
275
- });
276
-
277
- updatePagination(data);
278
- }
279
-
280
- function createDissertationRow(dissertation) {
281
- const row = document.createElement('tr');
282
- row.className = 'table-row border-b border-white/10';
283
-
284
- const date = new Date(dissertation.created_at).toLocaleDateString('fr-FR', {
285
- year: 'numeric',
286
- month: 'short',
287
- day: 'numeric',
288
- hour: '2-digit',
289
- minute: '2-digit'
290
- });
291
-
292
- row.innerHTML = `
293
- <td class="p-4">${date}</td>
294
- <td class="p-4">
295
- <div class="max-w-xs truncate" title="${dissertation.question}">
296
- ${dissertation.question}
297
- </div>
298
- </td>
299
- <td class="p-4">
300
- <span class="px-2 py-1 rounded text-xs ${dissertation.dissertation_type === 'type1' ? 'bg-blue-500/20 text-blue-300' : 'bg-green-500/20 text-green-300'}">
301
- ${dissertation.dissertation_type.toUpperCase()}
302
- </span>
303
- </td>
304
- <td class="p-4">
305
- <div class="max-w-xs truncate" title="${dissertation.course_title || 'Aucun'}">
306
- ${dissertation.course_title || '-'}
307
- </div>
308
- </td>
309
- <td class="p-4">${dissertation.user_ip}</td>
310
- <td class="p-4">
311
- <div class="flex gap-2">
312
- <button onclick="viewDissertation(${dissertation.id})"
313
- class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors">
314
- 👁️ Voir
315
- </button>
316
- <button onclick="deleteDissertation(${dissertation.id})"
317
- class="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-sm transition-colors">
318
- 🗑️ Suppr.
319
- </button>
320
- </div>
321
- </td>
322
- `;
323
-
324
- return row;
325
- }
326
-
327
- function updatePagination(data) {
328
- const { page, total_pages, total, per_page } = data;
329
- const start = (page - 1) * per_page + 1;
330
- const end = Math.min(page * per_page, total);
331
-
332
- document.getElementById('paginationInfo').textContent =
333
- `Affichage de ${start} à ${end} sur ${total} résultats`;
334
-
335
- // Boutons précédent/suivant
336
- const prevBtn = document.getElementById('prevBtn');
337
- const nextBtn = document.getElementById('nextBtn');
338
-
339
- prevBtn.disabled = page <= 1;
340
- nextBtn.disabled = page >= total_pages;
341
-
342
- prevBtn.onclick = () => { if (page > 1) { currentPage = page - 1; loadDissertations(); } };
343
- nextBtn.onclick = () => { if (page < total_pages) { currentPage = page + 1; loadDissertations(); } };
344
-
345
- // Numéros de page
346
- const pageNumbers = document.getElementById('pageNumbers');
347
- pageNumbers.innerHTML = '';
348
-
349
- const maxVisible = 5;
350
- let startPage = Math.max(1, page - Math.floor(maxVisible / 2));
351
- let endPage = Math.min(total_pages, startPage + maxVisible - 1);
352
-
353
- if (endPage - startPage + 1 < maxVisible) {
354
- startPage = Math.max(1, endPage - maxVisible + 1);
355
- }
356
-
357
- for (let i = startPage; i <= endPage; i++) {
358
- const pageBtn = document.createElement('button');
359
- pageBtn.className = `px-3 py-2 rounded-lg transition-colors ${
360
- i === page
361
- ? 'bg-white/30 text-white'
362
- : 'bg-white/10 hover:bg-white/20 text-white/80'
363
- }`;
364
- pageBtn.textContent = i;
365
- pageBtn.onclick = () => { currentPage = i; loadDissertations(); };
366
- pageNumbers.appendChild(pageBtn);
367
- }
368
- }
369
-
370
- function updateStats(data) {
371
- // Simuler des stats basiques (vous pouvez créer une API dédiée pour des stats plus précises)
372
- document.getElementById('totalDissertations').textContent = data.total;
373
-
374
- // Compter les types dans la page actuelle (approximation)
375
- const type1Count = data.dissertations.filter(d => d.dissertation_type === 'type1').length;
376
- const type2Count = data.dissertations.filter(d => d.dissertation_type === 'type2').length;
377
-
378
- document.getElementById('type1Count').textContent = type1Count;
379
- document.getElementById('type2Count').textContent = type2Count;
380
-
381
- // Compter aujourd'hui (approximation basée sur la page actuelle)
382
- const today = new Date().toDateString();
383
- const todayCount = data.dissertations.filter(d =>
384
- new Date(d.created_at).toDateString() === today
385
- ).length;
386
-
387
- document.getElementById('todayCount').textContent = todayCount;
388
- }
389
-
390
- async function viewDissertation(id) {
391
- try {
392
- const response = await fetch(`/api/dissertations/${id}`);
393
- const data = await response.json();
394
-
395
- if (response.ok) {
396
- showDissertationModal(data);
397
- } else {
398
- throw new Error(data.error || 'Erreur lors du chargement');
399
- }
400
- } catch (error) {
401
- console.error('Erreur:', error);
402
- showError('Erreur lors du chargement du détail');
403
- }
404
- }
405
-
406
- function showDissertationModal(dissertation) {
407
- const content = dissertation.generated_content;
408
- const modalContent = document.getElementById('modalContent');
409
-
410
- modalContent.innerHTML = `
411
- <div class="space-y-6">
412
- <!-- Info générales -->
413
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-white/10 rounded-lg">
414
- <div>
415
- <strong class="text-white/90">Question:</strong>
416
- <p class="text-white/80 mt-1">${dissertation.question}</p>
417
- </div>
418
- <div>
419
- <strong class="text-white/90">Type:</strong>
420
- <span class="ml-2 px-2 py-1 rounded text-xs ${
421
- dissertation.dissertation_type === 'type1'
422
- ? 'bg-blue-500/20 text-blue-300'
423
- : 'bg-green-500/20 text-green-300'
424
- }">
425
- ${dissertation.dissertation_type.toUpperCase()}
426
- </span>
427
- </div>
428
- <div>
429
- <strong class="text-white/90">Cours:</strong>
430
- <p class="text-white/80 mt-1">${dissertation.course_title || 'Aucun cours spécifique'}</p>
431
- </div>
432
- <div>
433
- <strong class="text-white/90">Date:</strong>
434
- <p class="text-white/80 mt-1">${new Date(dissertation.created_at).toLocaleString('fr-FR')}</p>
435
- </div>
436
- <div>
437
- <strong class="text-white/90">IP Utilisateur:</strong>
438
- <p class="text-white/80 mt-1">${dissertation.user_ip}</p>
439
- </div>
440
- <div>
441
- <strong class="text-white/90">User Agent:</strong>
442
- <p class="text-white/80 mt-1 text-xs truncate" title="${dissertation.user_agent}">${dissertation.user_agent}</p>
443
  </div>
 
 
 
 
444
  </div>
445
 
446
- <!-- Contenu de la dissertation -->
447
- <div class="bg-white/5 rounded-lg p-4">
448
- <h4 class="text-lg font-semibold text-white mb-4">📝 Contenu de la Dissertation</h4>
449
-
450
- <!-- Introduction -->
451
- <div class="mb-6">
452
- <h5 class="font-medium text-white/90 mb-2">Introduction</h5>
453
- <div class="bg-white/10 rounded p-3 text-white/80 text-sm leading-relaxed">
454
- ${content.introduction}
 
 
 
 
 
 
455
  </div>
456
  </div>
 
457
 
458
- <!-- Parties -->
459
- <div class="space-y-6">
460
- ${content.parties.map((partie, index) => `
461
- <div class="mb-6">
462
- <h5 class="font-medium text-white/90 mb-2">Partie ${index + 1}</h5>
463
-
464
- <!-- Chapeau -->
465
- <div class="bg-blue-500/20 rounded p-3 mb-3">
466
- <p class="text-blue-200 text-sm font-medium mb-1">Chapeau:</p>
467
- <p class="text-white/80 text-sm">${partie.chapeau}</p>
468
- </div>
469
-
470
- <!-- Arguments -->
471
- <div class="space-y-2">
472
- ${partie.arguments.map((arg, argIndex) => `
473
- <div class="bg-white/10 rounded p-3">
474
- <p class="text-green-300 text-xs mb-1">Argument ${argIndex + 1}:</p>
475
- <p class="text-white/80 text-sm leading-relaxed">${arg.paragraphe_argumentatif}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  </div>
477
- `).join('')}
478
- </div>
479
-
480
- <!-- Transition -->
481
- ${partie.transition ? `
482
- <div class="bg-purple-500/20 rounded p-3 mt-3">
483
- <p class="text-purple-200 text-sm font-medium mb-1">Transition:</p>
484
- <p class="text-white/80 text-sm">${partie.transition}</p>
485
  </div>
486
- ` : ''}
487
  </div>
488
- `).join('')}
489
  </div>
 
490
 
491
- <!-- Conclusion -->
492
- <div class="mt-6">
493
- <h5 class="font-medium text-white/90 mb-2">Conclusion</h5>
494
- <div class="bg-white/10 rounded p-3 text-white/80 text-sm leading-relaxed">
495
- ${content.conclusion}
496
- </div>
497
  </div>
498
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
- <!-- Actions -->
501
- <div class="flex gap-4 pt-4">
502
- <button onclick="generatePDF(${dissertation.id})"
503
- class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
504
- 📄 Générer PDF
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  </button>
506
- <button onclick="copyToClipboard(${JSON.stringify(content).replace(/"/g, '&quot;')})"
507
- class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
508
- 📋 Copier le texte
509
  </button>
510
  </div>
511
  </div>
512
- `;
513
-
514
- document.getElementById('detailModal').classList.remove('hidden');
515
- document.body.style.overflow = 'hidden';
516
- }
517
-
518
- function closeModal() {
519
- document.getElementById('detailModal').classList.add('hidden');
520
- document.body.style.overflow = 'auto';
521
- }
522
-
523
- function deleteDissertation(id) {
524
- deleteId = id;
525
- document.getElementById('deleteModal').classList.remove('hidden');
526
- }
527
 
528
- function closeDeleteModal() {
529
- document.getElementById('deleteModal').classList.add('hidden');
530
- deleteId = null;
531
- }
532
 
533
- async function confirmDelete() {
534
- if (!deleteId) return;
535
-
536
- try {
537
- const response = await fetch(`/api/dissertations/${deleteId}`, {
538
- method: 'DELETE'
539
- });
540
-
541
- const data = await response.json();
542
-
543
- if (response.ok) {
544
- showSuccess('Dissertation supprimée avec succès');
545
- closeDeleteModal();
546
- loadDissertations(); // Recharger la liste
547
- } else {
548
- throw new Error(data.error || 'Erreur lors de la suppression');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  }
550
- } catch (error) {
551
- console.error('Erreur:', error);
552
- showError('Erreur lors de la suppression');
553
  }
554
  }
555
-
556
- function generatePDF(dissertationId) {
557
- // Placeholder pour la génération PDF
558
- showInfo('Fonctionnalité PDF en développement');
559
- }
560
-
561
- function copyToClipboard(content) {
562
- const textContent = formatDissertationText(content);
563
- navigator.clipboard.writeText(textContent).then(() => {
564
- showSuccess('Texte copié dans le presse-papiers');
565
- }).catch(() => {
566
- showError('Erreur lors de la copie');
567
- });
568
- }
569
-
570
- function formatDissertationText(content) {
571
- let text = `DISSERTATION DE PHILOSOPHIE\n`;
572
- text += `Sujet: ${content.sujet}\n`;
573
- text += `Professeur: ${content.prof}\n\n`;
574
-
575
- text += `INTRODUCTION\n`;
576
- text += `${content.introduction}\n\n`;
577
-
578
- content.parties.forEach((partie, index) => {
579
- text += `PARTIE ${index + 1}\n`;
580
- text += `${partie.chapeau}\n\n`;
581
-
582
- partie.arguments.forEach((arg, argIndex) => {
583
- text += `${arg.paragraphe_argumentatif}\n\n`;
584
- });
585
-
586
- if (partie.transition) {
587
- text += `${partie.transition}\n\n`;
588
- }
589
- });
590
-
591
- text += `CONCLUSION\n`;
592
- text += `${content.conclusion}\n`;
593
-
594
- return text;
595
- }
596
-
597
- function showSuccess(message) {
598
- showNotification(message, 'bg-green-500');
599
- }
600
-
601
- function showError(message) {
602
- showNotification(message, 'bg-red-500');
603
- }
604
-
605
- function showInfo(message) {
606
- showNotification(message, 'bg-blue-500');
607
- }
608
-
609
- function showNotification(message, bgColor) {
610
- const notification = document.createElement('div');
611
- notification.className = `fixed top-4 right-4 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg z-50 fade-in`;
612
- notification.textContent = message;
613
-
614
- document.body.appendChild(notification);
615
-
616
- setTimeout(() => {
617
- notification.remove();
618
- }, 3000);
619
- }
620
  </script>
621
  </body>
622
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Gestion des Dissertations - Mariam AI</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
9
  <style>
10
+ [x-cloak] { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  </style>
12
  </head>
13
+ <body class="bg-gray-50 min-h-screen">
14
+ <div x-data="gestionApp()" x-init="loadData()" class="container mx-auto px-4 py-8">
15
+
16
  <!-- Header -->
17
+ <div class="bg-white rounded-lg shadow-md p-6 mb-8">
18
+ <div class="flex justify-between items-center">
19
+ <div>
20
+ <h1 class="text-3xl font-bold text-gray-800">Gestion des Dissertations</h1>
21
+ <p class="text-gray-600 mt-2">Suivi des requêtes et réponses générées par Mariam AI</p>
22
+ </div>
23
+ <div class="flex space-x-4">
24
+ <a href="/" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition duration-200">
25
+ ← Retour à l'app
26
+ </a>
27
+ <button @click="confirmClearAll()" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition duration-200">
28
+ Vider tout
29
+ </button>
30
+ </div>
31
+ </div>
32
  </div>
33
 
34
+ <!-- Statistiques -->
35
  <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
36
+ <div class="bg-white p-6 rounded-lg shadow-md">
37
+ <div class="flex items-center">
38
+ <div class="p-3 rounded-full bg-blue-100 text-blue-500">
39
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
40
+ <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>
41
+ </svg>
42
+ </div>
43
+ <div class="ml-4">
44
+ <p class="text-sm font-medium text-gray-600">Total</p>
45
+ <p class="text-2xl font-semibold text-gray-900" x-text="stats.total"></p>
46
+ </div>
47
+ </div>
48
  </div>
49
+
50
+ <div class="bg-white p-6 rounded-lg shadow-md">
51
+ <div class="flex items-center">
52
+ <div class="p-3 rounded-full bg-green-100 text-green-500">
53
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
55
+ </svg>
56
+ </div>
57
+ <div class="ml-4">
58
+ <p class="text-sm font-medium text-gray-600">Succès</p>
59
+ <p class="text-2xl font-semibold text-gray-900" x-text="stats.success"></p>
60
+ </div>
61
+ </div>
62
  </div>
63
+
64
+ <div class="bg-white p-6 rounded-lg shadow-md">
65
+ <div class="flex items-center">
66
+ <div class="p-3 rounded-full bg-red-100 text-red-500">
67
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
68
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
69
+ </svg>
70
+ </div>
71
+ <div class="ml-4">
72
+ <p class="text-sm font-medium text-gray-600">Erreurs</p>
73
+ <p class="text-2xl font-semibold text-gray-900" x-text="stats.errors"></p>
74
+ </div>
75
+ </div>
76
  </div>
77
+
78
+ <div class="bg-white p-6 rounded-lg shadow-md">
79
+ <div class="flex items-center">
80
+ <div class="p-3 rounded-full bg-purple-100 text-purple-500">
81
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
82
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
83
+ </svg>
84
+ </div>
85
+ <div class="ml-4">
86
+ <p class="text-sm font-medium text-gray-600">Taux de succès</p>
87
+ <p class="text-2xl font-semibold text-gray-900" x-text="stats.successRate + '%'"></p>
88
+ </div>
89
+ </div>
90
  </div>
91
  </div>
92
 
93
+ <!-- Filtres -->
94
+ <div class="bg-white rounded-lg shadow-md p-6 mb-8">
95
+ <div class="flex flex-wrap gap-4 items-center">
96
+ <div>
97
+ <label class="block text-sm font-medium text-gray-700 mb-2">Filtrer par statut</label>
98
+ <select x-model="filter.status" @change="applyFilters()" class="border border-gray-300 rounded-md px-3 py-2">
99
+ <option value="">Tous</option>
100
+ <option value="success">Succès</option>
101
+ <option value="error">Erreurs</option>
102
+ </select>
103
  </div>
104
  <div>
105
+ <label class="block text-sm font-medium text-gray-700 mb-2">Filtrer par type</label>
106
+ <select x-model="filter.type" @change="applyFilters()" class="border border-gray-300 rounded-md px-3 py-2">
107
+ <option value="">Tous</option>
108
  <option value="type1">Type 1</option>
109
  <option value="type2">Type 2</option>
110
  </select>
111
  </div>
112
  <div>
113
+ <label class="block text-sm font-medium text-gray-700 mb-2">Rechercher</label>
114
+ <input type="text" x-model="filter.search" @input="applyFilters()"
115
+ placeholder="Rechercher dans les sujets..."
116
+ class="border border-gray-300 rounded-md px-3 py-2">
117
  </div>
118
+ <div class="flex-1"></div>
119
+ <button @click="refreshData()" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition duration-200">
120
+ 🔄 Actualiser
121
+ </button>
122
  </div>
123
  </div>
124
 
125
+ <!-- Messages -->
126
+ <div x-show="message.show" x-cloak
127
+ :class="message.type === 'success' ? 'bg-green-100 border-green-500 text-green-700' : 'bg-red-100 border-red-500 text-red-700'"
128
+ class="border-l-4 p-4 mb-6 rounded">
129
+ <p x-text="message.text"></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  </div>
 
131
 
132
+ <!-- Loading -->
133
+ <div x-show="loading" x-cloak class="text-center py-8">
134
+ <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
135
+ <p class="mt-2 text-gray-600">Chargement...</p>
 
 
 
 
 
 
136
  </div>
 
137
 
138
+ <!-- Liste des dissertations -->
139
+ <div x-show="!loading" x-cloak class="space-y-6">
140
+ <template x-for="item in filteredData" :key="item.id">
141
+ <div class="bg-white rounded-lg shadow-md p-6">
142
+ <div class="flex justify-between items-start mb-4">
143
+ <div class="flex-1">
144
+ <div class="flex items-center gap-3 mb-2">
145
+ <span class="text-sm text-gray-500" x-text="formatDate(item.timestamp)"></span>
146
+ <span :class="item.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
147
+ class="px-2 py-1 rounded-full text-xs font-medium"
148
+ x-text="item.success ? 'Succès' : 'Erreur'">
149
+ </span>
150
+ <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-xs font-medium"
151
+ x-text="item.input.type">
152
+ </span>
153
+ </div>
154
+ <h3 class="text-lg font-semibold text-gray-800 mb-2" x-text="item.input.question"></h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  </div>
156
+ <button @click="deleteRecord(item.id)"
157
+ class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm transition duration-200">
158
+ Supprimer
159
+ </button>
160
  </div>
161
 
162
+ <!-- Détails de l'input -->
163
+ <div class="bg-gray-50 rounded-lg p-4 mb-4">
164
+ <h4 class="font-medium text-gray-700 mb-2">Données d'entrée</h4>
165
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
166
+ <div>
167
+ <span class="font-medium">Question:</span>
168
+ <p class="text-gray-600 mt-1" x-text="item.input.question"></p>
169
+ </div>
170
+ <div>
171
+ <span class="font-medium">Type:</span>
172
+ <p class="text-gray-600 mt-1" x-text="item.input.type"></p>
173
+ </div>
174
+ <div>
175
+ <span class="font-medium">Cours ID:</span>
176
+ <p class="text-gray-600 mt-1" x-text="item.input.courseId || 'Aucun'"></p>
177
  </div>
178
  </div>
179
+ </div>
180
 
181
+ <!-- Résultat ou erreur -->
182
+ <div x-show="item.success" x-cloak>
183
+ <div class="border-l-4 border-green-500 bg-green-50 p-4 rounded">
184
+ <h4 class="font-medium text-green-800 mb-2">Résultat généré</h4>
185
+ <div class="space-y-2 text-sm">
186
+ <div><strong>Sujet:</strong> <span x-text="item.output?.sujet"></span></div>
187
+ <div><strong>Parties:</strong> <span x-text="item.output?.parties?.length + ' partie(s)'"></span></div>
188
+ <button @click="toggleDetails(item.id)"
189
+ class="text-green-600 hover:text-green-800 underline"
190
+ x-text="item.showDetails ? 'Masquer les détails' : 'Voir les détails'">
191
+ </button>
192
+
193
+ <!-- Détails complets -->
194
+ <div x-show="item.showDetails" x-cloak class="mt-4 p-4 bg-white rounded border">
195
+ <div class="space-y-4 max-h-96 overflow-y-auto">
196
+ <div>
197
+ <strong class="text-gray-700">Introduction:</strong>
198
+ <p class="mt-1 text-gray-600 text-sm" x-text="item.output?.introduction"></p>
199
+ </div>
200
+
201
+ <template x-for="(partie, index) in item.output?.parties" :key="index">
202
+ <div class="border-l-2 border-blue-200 pl-4">
203
+ <strong class="text-gray-700" x-text="'Partie ' + (index + 1) + ':'"></strong>
204
+ <p class="mt-1 text-gray-600 text-sm" x-text="partie.chapeau"></p>
205
+ <div class="mt-2">
206
+ <template x-for="(arg, argIndex) in partie.arguments" :key="argIndex">
207
+ <p class="mt-2 text-gray-600 text-sm pl-4 border-l border-gray-200"
208
+ x-text="arg.paragraphe_argumentatif"></p>
209
+ </template>
210
+ </div>
211
+ <p x-show="partie.transition" class="mt-2 text-gray-500 text-sm italic"
212
+ x-text="partie.transition"></p>
213
  </div>
214
+ </template>
215
+
216
+ <div>
217
+ <strong class="text-gray-700">Conclusion:</strong>
218
+ <p class="mt-1 text-gray-600 text-sm" x-text="item.output?.conclusion"></p>
 
 
 
219
  </div>
220
+ </div>
221
  </div>
222
+ </div>
223
  </div>
224
+ </div>
225
 
226
+ <div x-show="!item.success" x-cloak>
227
+ <div class="border-l-4 border-red-500 bg-red-50 p-4 rounded">
228
+ <h4 class="font-medium text-red-800 mb-2">Erreur</h4>
229
+ <p class="text-red-700 text-sm" x-text="item.error"></p>
 
 
230
  </div>
231
  </div>
232
+ </div>
233
+ </template>
234
+
235
+ <!-- Message si aucune donnée -->
236
+ <div x-show="filteredData.length === 0" x-cloak class="text-center py-12">
237
+ <div class="text-gray-400 mb-4">
238
+ <svg class="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
239
+ <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>
240
+ </svg>
241
+ </div>
242
+ <h3 class="text-lg font-medium text-gray-900">Aucune donnée trouvée</h3>
243
+ <p class="text-gray-500">Aucune dissertation n'a encore été générée ou ne correspond aux filtres.</p>
244
+ </div>
245
+ </div>
246
 
247
+ <!-- Modal de confirmation -->
248
+ <div x-show="showConfirmModal" x-cloak
249
+ class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
250
+ <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
251
+ <div class="mt-3 text-center">
252
+ <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
253
+ <svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
+ <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>
255
+ </svg>
256
+ </div>
257
+ <h3 class="text-lg leading-6 font-medium text-gray-900 mt-2">Confirmer la suppression</h3>
258
+ <div class="mt-2 px-7 py-3">
259
+ <p class="text-sm text-gray-500">
260
+ Êtes-vous sûr de vouloir supprimer toutes les données ? Cette action est irréversible.
261
+ </p>
262
+ </div>
263
+ <div class="flex justify-center space-x-4 mt-4">
264
+ <button @click="showConfirmModal = false"
265
+ class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400">
266
+ Annuler
267
  </button>
268
+ <button @click="clearAllData()"
269
+ class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">
270
+ Supprimer tout
271
  </button>
272
  </div>
273
  </div>
274
+ </div>
275
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
+ </div>
 
 
 
278
 
279
+ <script>
280
+ function gestionApp() {
281
+ return {
282
+ data: [],
283
+ filteredData: [],
284
+ loading: false,
285
+ showConfirmModal: false,
286
+ filter: {
287
+ status: '',
288
+ type: '',
289
+ search: ''
290
+ },
291
+ stats: {
292
+ total: 0,
293
+ success: 0,
294
+ errors: 0,
295
+ successRate: 0
296
+ },
297
+ message: {
298
+ show: false,
299
+ text: '',
300
+ type: 'success'
301
+ },
302
+
303
+ async loadData() {
304
+ this.loading = true;
305
+ try {
306
+ const response = await fetch('/api/gestion/dissertations');
307
+ const result = await response.json();
308
+
309
+ if (result.success) {
310
+ this.data = result.data;
311
+ this.calculateStats();
312
+ this.applyFilters();
313
+ } else {
314
+ this.showMessage('Erreur lors du chargement des données', 'error');
315
+ }
316
+ } catch (error) {
317
+ console.error('Erreur:', error);
318
+ this.showMessage('Erreur de connexion', 'error');
319
+ } finally {
320
+ this.loading = false;
321
+ }
322
+ },
323
+
324
+ async refreshData() {
325
+ await this.loadData();
326
+ this.showMessage('Données actualisées', 'success');
327
+ },
328
+
329
+ calculateStats() {
330
+ this.stats.total = this.data.length;
331
+ this.stats.success = this.data.filter(item => item.success).length;
332
+ this.stats.errors = this.data.filter(item => !item.success).length;
333
+ this.stats.successRate = this.stats.total > 0 ?
334
+ Math.round((this.stats.success / this.stats.total) * 100) : 0;
335
+ },
336
+
337
+ applyFilters() {
338
+ let filtered = [...this.data];
339
+
340
+ // Filtre par statut
341
+ if (this.filter.status) {
342
+ if (this.filter.status === 'success') {
343
+ filtered = filtered.filter(item => item.success);
344
+ } else if (this.filter.status === 'error') {
345
+ filtered = filtered.filter(item => !item.success);
346
+ }
347
+ }
348
+
349
+ // Filtre par type
350
+ if (this.filter.type) {
351
+ filtered = filtered.filter(item => item.input.type === this.filter.type);
352
+ }
353
+
354
+ // Filtre par recherche
355
+ if (this.filter.search) {
356
+ const searchLower = this.filter.search.toLowerCase();
357
+ filtered = filtered.filter(item =>
358
+ item.input.question.toLowerCase().includes(searchLower)
359
+ );
360
+ }
361
+
362
+ // Trier par date (plus récent d'abord)
363
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
364
+
365
+ this.filteredData = filtered;
366
+ },
367
+
368
+ async deleteRecord(id) {
369
+ if (!confirm('Êtes-vous sûr de vouloir supprimer cet enregistrement ?')) {
370
+ return;
371
+ }
372
+
373
+ try {
374
+ const response = await fetch(`/api/gestion/dissertations/${id}`, {
375
+ method: 'DELETE'
376
+ });
377
+ const result = await response.json();
378
+
379
+ if (result.success) {
380
+ await this.loadData();
381
+ this.showMessage('Enregistrement supprimé', 'success');
382
+ } else {
383
+ this.showMessage('Erreur lors de la suppression', 'error');
384
+ }
385
+ } catch (error) {
386
+ console.error('Erreur:', error);
387
+ this.showMessage('Erreur de connexion', 'error');
388
+ }
389
+ },
390
+
391
+ confirmClearAll() {
392
+ this.showConfirmModal = true;
393
+ },
394
+
395
+ async clearAllData() {
396
+ this.showConfirmModal = false;
397
+
398
+ try {
399
+ const response = await fetch('/api/gestion/dissertations/clear', {
400
+ method: 'DELETE'
401
+ });
402
+ const result = await response.json();
403
+
404
+ if (result.success) {
405
+ await this.loadData();
406
+ this.showMessage('Toutes les données ont été supprimées', 'success');
407
+ } else {
408
+ this.showMessage('Erreur lors de la suppression', 'error');
409
+ }
410
+ } catch (error) {
411
+ console.error('Erreur:', error);
412
+ this.showMessage('Erreur de connexion', 'error');
413
+ }
414
+ },
415
+
416
+ toggleDetails(id) {
417
+ const item = this.data.find(item => item.id === id);
418
+ if (item) {
419
+ item.showDetails = !item.showDetails;
420
+ }
421
+ },
422
+
423
+ formatDate(timestamp) {
424
+ const date = new Date(timestamp);
425
+ return date.toLocaleString('fr-FR', {
426
+ year: 'numeric',
427
+ month: '2-digit',
428
+ day: '2-digit',
429
+ hour: '2-digit',
430
+ minute: '2-digit'
431
+ });
432
+ },
433
+
434
+ showMessage(text, type = 'success') {
435
+ this.message.text = text;
436
+ this.message.type = type;
437
+ this.message.show = true;
438
+
439
+ setTimeout(() => {
440
+ this.message.show = false;
441
+ }, 3000);
442
  }
 
 
 
443
  }
444
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  </script>
446
  </body>
447
  </html>