OUAREDAEK commited on
Commit
a08c15b
·
verified ·
1 Parent(s): c180506

Upload cross_reading/cross_readers.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. cross_reading/cross_readers.js +1229 -0
cross_reading/cross_readers.js ADDED
@@ -0,0 +1,1229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ================= VARIABLES GLOBALES =================
2
+ let allEvaluations = [];
3
+ let scenariosData = [];
4
+ let filteredEvaluations = [];
5
+ let reviewsData = [];
6
+ let currentReviewEvalId = null;
7
+ let chart1 = null;
8
+ let chart2 = null;
9
+
10
+ // ================= INITIALISATION =================
11
+ document.addEventListener('DOMContentLoaded', function() {
12
+ console.log('Initialisation de l\'analyse croisée...');
13
+ loadData();
14
+ });
15
+
16
+ // ================= CHARGEMENT DES DONNÉES =================
17
+ function loadData() {
18
+ console.log('Début du chargement des données...');
19
+
20
+ // Afficher le statut de chargement
21
+ showLoadingMessage('Chargement des données...');
22
+
23
+ // Masquer la section d'aide
24
+ document.getElementById('helpSection').style.display = 'none';
25
+
26
+ // Charger les évaluations
27
+ fetch('/api/evaluations')
28
+ .then(response => {
29
+ if (!response.ok) {
30
+ console.warn('API /api/evaluations non disponible, utilisation des données locales');
31
+ // Charger depuis le fichier local
32
+ return fetch('trust_stats.json').then(r => r.json());
33
+ }
34
+ return response.json();
35
+ })
36
+ .then(data => {
37
+ allEvaluations = data.evaluations || [];
38
+ console.log(`${allEvaluations.length} évaluations chargées`);
39
+
40
+ if (allEvaluations.length === 0) {
41
+ console.warn('Aucune évaluation trouvée');
42
+ showWarningMessage('Aucune donnée d\'évaluation trouvée. Générer des données de test pour commencer.');
43
+ document.getElementById('helpSection').style.display = 'block';
44
+ } else {
45
+ document.getElementById('warningMessage').style.display = 'none';
46
+ }
47
+
48
+ // Charger les scénarios
49
+ return fetch('/api/scenarios')
50
+ .then(r => r.ok ? r.json() : fetch('scenarios.json').then(r2 => r2.json()))
51
+ .catch(() => fetch('scenarios.json').then(r => r.json()));
52
+ })
53
+ .then(data => {
54
+ scenariosData = data.scenarios || [];
55
+ console.log(`${scenariosData.length} scénarios chargés`);
56
+
57
+ // Charger les reviews
58
+ return fetch('/api/reviews')
59
+ .then(r => r.ok ? r.json() : fetch('review.json').then(r2 => r2.json()))
60
+ .catch(() => fetch('review.json').then(r => r.json()));
61
+ })
62
+ .then(data => {
63
+ reviewsData = data.reviews || [];
64
+ console.log(`${reviewsData.length} reviews chargés`);
65
+
66
+ // Traiter les données
67
+ processEvaluations();
68
+ showSuccessMessage('Données chargées avec succès !');
69
+ })
70
+ .catch(error => {
71
+ console.error('Erreur de chargement:', error);
72
+ showErrorMessage(`Erreur de chargement: ${error.message}<br>
73
+ <br>
74
+ <strong>Solutions possibles :</strong><br>
75
+ 1. Vérifiez que les fichiers JSON existent<br>
76
+ 2. Générez des données de test avec le bouton ci-dessous<br>
77
+ 3. Vérifiez la console du navigateur (F12) pour plus de détails<br>
78
+ <br>
79
+ <button onclick="generateTestData()" style="margin-top:10px;padding:10px 20px;background:#4CAF50;color:white;border:none;border-radius:5px;cursor:pointer;font-weight:bold;">
80
+ 🧪 Générer des données de test
81
+ </button>
82
+ <button onclick="location.reload()" style="margin-top:10px;margin-left:10px;padding:10px 20px;background:#2196F3;color:white;border:none;border-radius:5px;cursor:pointer;">
83
+ 🔄 Réessayer
84
+ </button>`);
85
+
86
+ // Afficher la section d'aide
87
+ document.getElementById('helpSection').style.display = 'block';
88
+ });
89
+ }
90
+
91
+ // ================= GÉNÉRATION DE DONNÉES DE TEST =================
92
+ function generateTestData() {
93
+ console.log('Génération de données de test...');
94
+
95
+ showLoadingMessage('Génération des données de test...');
96
+
97
+ // Essayer d'abord l'API
98
+ fetch('/api/generate_test_data')
99
+ .then(response => {
100
+ if (!response.ok) {
101
+ // Si l'API n'est pas disponible, générer localement
102
+ return generateLocalTestData();
103
+ }
104
+ return response.json();
105
+ })
106
+ .then(data => {
107
+ console.log('Données de test générées:', data);
108
+ showSuccessMessage(`✅ Données de test générées avec succès !<br>
109
+ - ${data.evaluations || 5} évaluations<br>
110
+ - ${data.scenarios || scenariosData.length} scénarios<br>
111
+ - ${data.reviews || 3} reviews`);
112
+
113
+ // Recharger les données
114
+ setTimeout(() => {
115
+ location.reload();
116
+ }, 2000);
117
+ })
118
+ .catch(error => {
119
+ console.error('Erreur génération données test:', error);
120
+ generateLocalTestData();
121
+ });
122
+ }
123
+
124
+ function generateLocalTestData() {
125
+ // Générer des évaluations de test
126
+ const testEvaluations = [];
127
+ const evaluators = ['Expert A', 'Expert B', 'Expert C', 'Anonyme'];
128
+ const roles = ['Professeur', 'Psychologue', 'Tuteur', 'Chercheur'];
129
+
130
+ for (let i = 0; i < 10; i++) {
131
+ const scenarioId = Math.floor(Math.random() * 16) + 1;
132
+ const evaluatorIndex = Math.floor(Math.random() * evaluators.length);
133
+
134
+ const metrics = {
135
+ AR: Math.floor(Math.random() * 6),
136
+ AE: Math.floor(Math.random() * 6),
137
+ ESR: Math.floor(Math.random() * 6),
138
+ SDM: Math.floor(Math.random() * 6),
139
+ SM: Math.floor(Math.random() * 6)
140
+ };
141
+
142
+ const totalScore = (metrics.AR + metrics.AE + metrics.ESR + metrics.SDM + metrics.SM) * 4;
143
+
144
+ testEvaluations.push({
145
+ id: `test_eval_${Date.now()}_${i}`,
146
+ scenario_id: scenarioId,
147
+ evaluator_name: evaluators[evaluatorIndex],
148
+ evaluator_role: roles[evaluatorIndex],
149
+ trust_metrics: metrics,
150
+ trust_score: totalScore,
151
+ comment: 'Évaluation générée automatiquement pour test',
152
+ timestamp: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
153
+ cross_reading: Math.random() > 0.7
154
+ });
155
+ }
156
+
157
+ // Générer des reviews de test
158
+ const testReviews = [];
159
+ for (let i = 0; i < 5; i++) {
160
+ if (testEvaluations[i]) {
161
+ const decisions = ['approved', 'needs_revision', 'rejected'];
162
+ const decision = decisions[Math.floor(Math.random() * decisions.length)];
163
+
164
+ testReviews.push({
165
+ id: `test_review_${Date.now()}_${i}`,
166
+ evaluation_id: testEvaluations[i].id,
167
+ scenario_id: testEvaluations[i].scenario_id,
168
+ evaluator_name: testEvaluations[i].evaluator_name,
169
+ evaluator_role: testEvaluations[i].evaluator_role,
170
+ original_score: testEvaluations[i].trust_score,
171
+ original_comment: testEvaluations[i].comment,
172
+ decision: decision,
173
+ review_comment: `Review automatique - ${decision === 'approved' ? 'Approuvé' : decision === 'needs_revision' ? 'Nécessite révision' : 'Rejeté'}`,
174
+ reviewer: 'Système de test',
175
+ timestamp: new Date().toISOString()
176
+ });
177
+ }
178
+ }
179
+
180
+ // Sauvegarder localement
181
+ try {
182
+ // Ajouter aux évaluations existantes
183
+ allEvaluations = [...allEvaluations, ...testEvaluations];
184
+
185
+ // Ajouter aux reviews existants
186
+ reviewsData = [...reviewsData, ...testReviews];
187
+
188
+ // Mettre à jour l'affichage
189
+ processEvaluations();
190
+ filterAllEvaluations();
191
+
192
+ showSuccessMessage(`✅ Données de test générées localement !<br>
193
+ - ${testEvaluations.length} nouvelles évaluations<br>
194
+ - ${testReviews.length} nouveaux reviews`);
195
+
196
+ return {
197
+ evaluations: testEvaluations.length,
198
+ reviews: testReviews.length,
199
+ scenarios: scenariosData.length
200
+ };
201
+ } catch (error) {
202
+ console.error('Erreur génération locale:', error);
203
+ showErrorMessage('Erreur lors de la génération des données de test');
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ // ================= TRAITEMENT DES DONNÉES =================
209
+ function processEvaluations() {
210
+ console.log('Traitement des évaluations...');
211
+
212
+ // Vérifier s'il y a des données
213
+ if (allEvaluations.length === 0) {
214
+ showWarningMessage('Aucune évaluation à afficher. Utilisez le bouton "Générer des données de test" pour commencer.');
215
+ document.getElementById('helpSection').style.display = 'block';
216
+ return;
217
+ }
218
+
219
+ // Nettoyer les données
220
+ allEvaluations.forEach((eval, index) => {
221
+ // Assurer un ID unique
222
+ if (!eval.id) {
223
+ eval.id = `eval_${Date.now()}_${index}_${Math.random().toString(36).substr(2, 9)}`;
224
+ }
225
+
226
+ // Normaliser les noms de métriques
227
+ if (eval.trust_metrics) {
228
+ const normalizedMetrics = {};
229
+ Object.keys(eval.trust_metrics).forEach(key => {
230
+ const normalizedKey = key.trim().toUpperCase();
231
+ if (normalizedKey === 'RA' || normalizedKey === 'AR') normalizedMetrics.AR = eval.trust_metrics[key];
232
+ else if (normalizedKey === 'EA' || normalizedKey === 'AE') normalizedMetrics.AE = eval.trust_metrics[key];
233
+ else if (normalizedKey === 'RE' || normalizedKey === 'ESR') normalizedMetrics.ESR = eval.trust_metrics[key];
234
+ else if (normalizedKey === 'MCS' || normalizedKey === 'SDM') normalizedMetrics.SDM = eval.trust_metrics[key];
235
+ else if (normalizedKey === 'MS' || normalizedKey === 'SM') normalizedMetrics.SM = eval.trust_metrics[key];
236
+ else normalizedMetrics[key] = eval.trust_metrics[key];
237
+ });
238
+ eval.trust_metrics = normalizedMetrics;
239
+ }
240
+
241
+ // Assurer que les métriques existent
242
+ if (!eval.trust_metrics) {
243
+ eval.trust_metrics = {
244
+ AR: 0, AE: 0, ESR: 0, SDM: 0, SM: 0
245
+ };
246
+ }
247
+
248
+ // Assurer un score par défaut
249
+ if (eval.trust_score === undefined || eval.trust_score === null) {
250
+ // Calculer le score moyen des métriques
251
+ const metrics = eval.trust_metrics;
252
+ const sum = (metrics.AR || 0) + (metrics.AE || 0) + (metrics.ESR || 0) + (metrics.SDM || 0) + (metrics.SM || 0);
253
+ eval.trust_score = Math.round((sum / 5) * 20); // Convertir 0-5 en 0-100
254
+ }
255
+
256
+ // Assurer un nom d'évaluateur
257
+ if (!eval.evaluator_name) {
258
+ eval.evaluator_name = 'Anonyme';
259
+ }
260
+
261
+ // Assurer une date
262
+ if (!eval.timestamp) {
263
+ eval.timestamp = new Date().toISOString();
264
+ }
265
+
266
+ // Assurer un scénario ID
267
+ if (!eval.scenario_id && eval.scenario_id !== 0) {
268
+ eval.scenario_id = Math.floor(Math.random() * 16) + 1;
269
+ }
270
+ });
271
+
272
+ // Calculer les statistiques globales
273
+ updateGlobalStats();
274
+
275
+ // Créer la liste des scénarios pour le filtre
276
+ populateScenarioFilter();
277
+
278
+ // Créer la liste des évaluateurs pour le filtre
279
+ populateEvaluatorFilter();
280
+
281
+ // Afficher les données initiales
282
+ filterData();
283
+ filterAllEvaluations();
284
+ }
285
+
286
+ // ================= STATISTIQUES GLOBALES =================
287
+ function updateGlobalStats() {
288
+ const totalEvaluations = allEvaluations.length;
289
+
290
+ // Compter les scénarios uniques
291
+ const uniqueScenarios = [...new Set(allEvaluations.map(e => e.scenario_id))].filter(id => id !== undefined && id !== null);
292
+
293
+ // Compter les évaluateurs uniques
294
+ const uniqueEvaluators = [...new Set(allEvaluations.map(e => e.evaluator_name || 'Anonyme'))];
295
+
296
+ // Calculer le taux de conflit global
297
+ let totalConflict = 0;
298
+ let analyzedScenarios = 0;
299
+
300
+ uniqueScenarios.forEach(scenarioId => {
301
+ const scenarioEvals = allEvaluations.filter(e => e.scenario_id === scenarioId);
302
+ if (scenarioEvals.length > 1) {
303
+ const avgScore = scenarioEvals.reduce((sum, e) => sum + (e.trust_score || 0), 0) / scenarioEvals.length;
304
+ const variance = scenarioEvals.reduce((sum, e) => sum + Math.pow((e.trust_score || 0) - avgScore, 2), 0) / scenarioEvals.length;
305
+ const stdDev = Math.sqrt(variance);
306
+ totalConflict += stdDev;
307
+ analyzedScenarios++;
308
+ }
309
+ });
310
+
311
+ const avgConflict = analyzedScenarios > 0 ? (totalConflict / analyzedScenarios) : 0;
312
+ const conflictRate = Math.min(100, Math.round(avgConflict));
313
+
314
+ // Mettre à jour l'interface
315
+ document.getElementById('totalEvaluations').textContent = totalEvaluations;
316
+ document.getElementById('totalScenarios').textContent = uniqueScenarios.length;
317
+ document.getElementById('totalEvaluators').textContent = uniqueEvaluators.length;
318
+ document.getElementById('conflictRate').textContent = `${conflictRate}%`;
319
+ }
320
+
321
+ // ================= FILTRES =================
322
+ function populateScenarioFilter() {
323
+ const filter = document.getElementById('scenarioFilter');
324
+ filter.innerHTML = '<option value="all">Tous les scénarios</option>';
325
+
326
+ // Trier les scénarios par ID
327
+ const uniqueScenarios = [...new Set(allEvaluations.map(e => e.scenario_id))]
328
+ .filter(id => id !== undefined && id !== null)
329
+ .sort((a, b) => a - b);
330
+
331
+ // Ajouter les options
332
+ uniqueScenarios.forEach(scenarioId => {
333
+ const scenario = scenariosData.find(s => s.id === scenarioId);
334
+ const option = document.createElement('option');
335
+ option.value = scenarioId;
336
+ option.textContent = scenario ? `Scénario ${scenarioId} - ${scenario.title}` : `Scénario ${scenarioId}`;
337
+ filter.appendChild(option);
338
+ });
339
+ }
340
+
341
+ function populateEvaluatorFilter() {
342
+ const filter = document.getElementById('evaluatorFilter');
343
+ filter.innerHTML = '<option value="all">Tous les évaluateurs</option>';
344
+
345
+ // Récupérer tous les évaluateurs uniques
346
+ const uniqueEvaluators = [...new Set(allEvaluations.map(e => e.evaluator_name || 'Anonyme'))]
347
+ .filter(name => name && name.trim() !== '')
348
+ .sort();
349
+
350
+ // Ajouter les options
351
+ uniqueEvaluators.forEach(evaluator => {
352
+ const option = document.createElement('option');
353
+ option.value = evaluator;
354
+ option.textContent = evaluator;
355
+ filter.appendChild(option);
356
+ });
357
+ }
358
+
359
+ function filterData() {
360
+ const scenarioFilter = document.getElementById('scenarioFilter').value;
361
+ const confidenceFilter = document.getElementById('confidenceFilter').value;
362
+ const sortBy = document.getElementById('sortBy').value;
363
+
364
+ // Filtrer par scénario
365
+ if (scenarioFilter === 'all') {
366
+ filteredEvaluations = allEvaluations;
367
+ } else {
368
+ filteredEvaluations = allEvaluations.filter(e => e.scenario_id === parseInt(scenarioFilter));
369
+ }
370
+
371
+ // Grouper les évaluations par scénario
372
+ const groupedByScenario = {};
373
+ filteredEvaluations.forEach(eval => {
374
+ if (eval.scenario_id === undefined) return;
375
+ if (!groupedByScenario[eval.scenario_id]) {
376
+ groupedByScenario[eval.scenario_id] = [];
377
+ }
378
+ groupedByScenario[eval.scenario_id].push(eval);
379
+ });
380
+
381
+ // Calculer les métriques pour chaque scénario
382
+ const scenarioMetrics = Object.keys(groupedByScenario).map(scenarioId => {
383
+ const evaluations = groupedByScenario[scenarioId];
384
+ const scenario = scenariosData.find(s => s.id === parseInt(scenarioId));
385
+
386
+ // Calculer la confiance moyenne
387
+ const avgScore = evaluations.reduce((sum, e) => sum + (e.trust_score || 0), 0) / evaluations.length;
388
+
389
+ // Calculer l'écart-type (niveau de conflit)
390
+ let stdDev = 0;
391
+ if (evaluations.length > 1) {
392
+ const variance = evaluations.reduce((sum, e) => sum + Math.pow((e.trust_score || 0) - avgScore, 2), 0) / evaluations.length;
393
+ stdDev = Math.sqrt(variance);
394
+ }
395
+
396
+ // Déterminer le niveau de conflit
397
+ let conflictLevel = 'low';
398
+ if (stdDev > 40) conflictLevel = 'high';
399
+ else if (stdDev > 20) conflictLevel = 'medium';
400
+
401
+ // Filtrer par niveau de conflit
402
+ if (confidenceFilter !== 'all' && conflictLevel !== confidenceFilter) {
403
+ return null;
404
+ }
405
+
406
+ return {
407
+ scenarioId: parseInt(scenarioId),
408
+ scenario: scenario,
409
+ evaluations: evaluations,
410
+ avgScore: avgScore,
411
+ stdDev: stdDev,
412
+ conflictLevel: conflictLevel,
413
+ minScore: Math.min(...evaluations.map(e => e.trust_score || 0)),
414
+ maxScore: Math.max(...evaluations.map(e => e.trust_score || 0)),
415
+ dateRange: {
416
+ min: new Date(Math.min(...evaluations.map(e => new Date(e.timestamp).getTime()))),
417
+ max: new Date(Math.max(...evaluations.map(e => new Date(e.timestamp).getTime())))
418
+ }
419
+ };
420
+ }).filter(metric => metric !== null);
421
+
422
+ // Trier selon le critère
423
+ scenarioMetrics.sort((a, b) => {
424
+ switch(sortBy) {
425
+ case 'scenario':
426
+ return a.scenarioId - b.scenarioId;
427
+ case 'conflict':
428
+ return b.stdDev - a.stdDev;
429
+ case 'evaluations':
430
+ return b.evaluations.length - a.evaluations.length;
431
+ case 'confidence':
432
+ return b.avgScore - a.avgScore;
433
+ default:
434
+ return a.scenarioId - b.scenarioId;
435
+ }
436
+ });
437
+
438
+ // Mettre à jour l'affichage
439
+ displayScenarioTable(scenarioMetrics);
440
+ updateCharts(scenarioMetrics);
441
+ }
442
+
443
+ // ================= AFFICHAGE DU TABLEAU PAR SCÉNARIO =================
444
+ function displayScenarioTable(scenarioMetrics) {
445
+ const tableContainer = document.getElementById('evaluationsTable');
446
+
447
+ if (scenarioMetrics.length === 0) {
448
+ tableContainer.innerHTML = `
449
+ <div style="text-align: center; padding: 40px; background: #FFF3E0; border-radius: 10px; color: #EF6C00;">
450
+ <p style="font-size: 18px;">Aucune évaluation correspondant aux filtres sélectionnés</p>
451
+ <p>Essayez de modifier vos critères de recherche</p>
452
+ </div>
453
+ `;
454
+ return;
455
+ }
456
+
457
+ let html = `
458
+ <table>
459
+ <thead>
460
+ <tr>
461
+ <th>Scénario</th>
462
+ <th>Évaluations</th>
463
+ <th>Confiance moyenne</th>
464
+ <th>Niveau de conflit</th>
465
+ <th>Plage de scores</th>
466
+ <th>Période</th>
467
+ <th>Actions</th>
468
+ </tr>
469
+ </thead>
470
+ <tbody>
471
+ `;
472
+
473
+ scenarioMetrics.forEach(metric => {
474
+ const conflictClass = `conflict-${metric.conflictLevel}`;
475
+ const conflictText = metric.conflictLevel === 'high' ? 'Élevé' :
476
+ metric.conflictLevel === 'medium' ? 'Moyen' : 'Faible';
477
+
478
+ html += `
479
+ <tr>
480
+ <td>
481
+ <strong>Scénario ${metric.scenarioId}</strong><br>
482
+ <small style="color:#666;">${metric.scenario ? metric.scenario.title : 'Non trouvé'}</small>
483
+ </td>
484
+ <td>
485
+ <div style="font-weight:bold;color:#1976D2;">${metric.evaluations.length}</div>
486
+ <small>${metric.evaluations.slice(0, 3).map(e => e.evaluator_name || 'Anonyme').join(', ')}${metric.evaluations.length > 3 ? '...' : ''}</small>
487
+ </td>
488
+ <td>
489
+ <div style="font-weight:bold;font-size:18px;color:#1976D2;">${Math.round(metric.avgScore)}%</div>
490
+ <div class="confidence-bar">
491
+ <div class="confidence-fill ${getConfidenceClass(metric.avgScore)}"
492
+ style="width: ${metric.avgScore}%"></div>
493
+ </div>
494
+ </td>
495
+ <td>
496
+ <div><span class="conflict-indicator ${conflictClass}"></span> ${conflictText}</div>
497
+ <small>Écart-type: ${metric.stdDev.toFixed(1)}%</small>
498
+ </td>
499
+ <td>
500
+ <div>${Math.round(metric.minScore)}% - ${Math.round(metric.maxScore)}%</div>
501
+ <small>Différence: ${Math.round(metric.maxScore - metric.minScore)}%</small>
502
+ </td>
503
+ <td>
504
+ <div>${formatDate(metric.dateRange.min)}</div>
505
+ <div>à ${formatDate(metric.dateRange.max)}</div>
506
+ </td>
507
+ <td>
508
+ <button onclick="showScenarioDetails(${metric.scenarioId})"
509
+ style="padding:5px 10px;background:#1976D2;color:white;border:none;border-radius:5px;cursor:pointer;">
510
+ Détails
511
+ </button>
512
+ </td>
513
+ </tr>
514
+ `;
515
+ });
516
+
517
+ html += `</tbody></table>`;
518
+ tableContainer.innerHTML = html;
519
+ }
520
+
521
+ // ================= TOUTES LES ÉVALUATIONS DÉTAILLÉES =================
522
+ function filterAllEvaluations() {
523
+ const evaluatorFilter = document.getElementById('evaluatorFilter').value;
524
+ const scoreFilter = document.getElementById('scoreFilter').value;
525
+ const sortBy = document.getElementById('sortEvaluations').value;
526
+
527
+ let filteredEvals = [...allEvaluations];
528
+
529
+ // Filtrer par évaluateur
530
+ if (evaluatorFilter !== 'all') {
531
+ filteredEvals = filteredEvals.filter(e =>
532
+ (e.evaluator_name || 'Anonyme') === evaluatorFilter
533
+ );
534
+ }
535
+
536
+ // Filtrer par score
537
+ if (scoreFilter !== 'all') {
538
+ filteredEvals = filteredEvals.filter(e => {
539
+ const score = e.trust_score || 0;
540
+ if (scoreFilter === 'high') return score >= 70;
541
+ if (scoreFilter === 'medium') return score >= 40 && score < 70;
542
+ if (scoreFilter === 'low') return score < 40;
543
+ return true;
544
+ });
545
+ }
546
+
547
+ // Trier
548
+ filteredEvals.sort((a, b) => {
549
+ switch(sortBy) {
550
+ case 'date_desc':
551
+ return new Date(b.timestamp) - new Date(a.timestamp);
552
+ case 'date_asc':
553
+ return new Date(a.timestamp) - new Date(b.timestamp);
554
+ case 'score_desc':
555
+ return (b.trust_score || 0) - (a.trust_score || 0);
556
+ case 'score_asc':
557
+ return (a.trust_score || 0) - (b.trust_score || 0);
558
+ case 'scenario':
559
+ return (a.scenario_id || 0) - (b.scenario_id || 0);
560
+ default:
561
+ return new Date(b.timestamp) - new Date(a.timestamp);
562
+ }
563
+ });
564
+
565
+ // Mettre à jour l'affichage
566
+ displayAllEvaluations(filteredEvals);
567
+ }
568
+
569
+ function displayAllEvaluations(evaluations) {
570
+ const container = document.getElementById('allEvaluationsTable');
571
+
572
+ if (evaluations.length === 0) {
573
+ container.innerHTML = `
574
+ <div style="text-align: center; padding: 40px; background: #FFF3E0; border-radius: 10px; color: #EF6C00;">
575
+ <p style="font-size: 18px;">Aucune évaluation correspondant aux filtres</p>
576
+ <p>Essayez de modifier les critères de filtrage</p>
577
+ </div>
578
+ `;
579
+ return;
580
+ }
581
+
582
+ let html = `
583
+ <table>
584
+ <thead>
585
+ <tr>
586
+ <th>Évaluateur</th>
587
+ <th>Scénario</th>
588
+ <th>Score</th>
589
+ <th class="metrics-table-cell">Métriques</th>
590
+ <th>Commentaire</th>
591
+ <th>Review</th>
592
+ <th>Actions</th>
593
+ </tr>
594
+ </thead>
595
+ <tbody>
596
+ `;
597
+
598
+ evaluations.forEach(eval => {
599
+ const scenario = scenariosData.find(s => s.id === eval.scenario_id);
600
+ const review = reviewsData.find(r => r.evaluation_id === eval.id);
601
+
602
+ // Vérifier si c'est une évaluation standard ou cross-reading
603
+ const isCrossReading = eval.cross_reading === true;
604
+
605
+ html += `
606
+ <tr style="${isCrossReading ? 'background: #E3F2FD;' : ''}">
607
+ <td>
608
+ <strong>${eval.evaluator_name || 'Anonyme'}</strong><br>
609
+ <small>${eval.evaluator_role || ''}</small><br>
610
+ <small style="color:#666;">${formatDate(new Date(eval.timestamp))}</small>
611
+ </td>
612
+ <td>
613
+ <strong>Scénario ${eval.scenario_id || 'N/A'}</strong><br>
614
+ <small style="color:#666;">${scenario ? scenario.title : 'Non trouvé'}</small>
615
+ ${isCrossReading ? '<div class="scenario-badge" style="background:#4CAF50;color:white;margin-top:3px;">Cross-Reading</div>' : ''}
616
+ </td>
617
+ <td>
618
+ <div style="font-weight:bold;font-size:18px;color:#1976D2;">${eval.trust_score || 0}%</div>
619
+ <div class="confidence-bar">
620
+ <div class="confidence-fill ${getConfidenceClass(eval.trust_score || 0)}"
621
+ style="width: ${eval.trust_score || 0}%"></div>
622
+ </div>
623
+ </td>
624
+ <td class="metrics-table-cell">
625
+ ${displayDetailedMetrics(eval.trust_metrics || {})}
626
+ </td>
627
+ <td style="max-width: 200px;">
628
+ <small style="color:#666;">${eval.comment || 'Aucun commentaire'}</small>
629
+ </td>
630
+ <td>
631
+ ${displayReviewStatus(review)}
632
+ </td>
633
+ <td>
634
+ <button onclick="openReviewModal('${eval.id}')"
635
+ style="padding:5px 10px;background:#4CAF50;color:white;border:none;border-radius:5px;cursor:pointer;font-size:12px;">
636
+ ✍️ Review
637
+ </button>
638
+ </td>
639
+ </tr>
640
+ `;
641
+ });
642
+
643
+ html += `</tbody></table>`;
644
+ container.innerHTML = html;
645
+ }
646
+
647
+ function displayDetailedMetrics(metrics) {
648
+ const metricDefinitions = {
649
+ 'AR': {
650
+ name: 'AR',
651
+ fullName: 'Authenticité Réflexive',
652
+ question: 'L\'analyse reflète-t-elle fidèlement la situation réelle de l\'étudiant ?'
653
+ },
654
+ 'AE': {
655
+ name: 'AE',
656
+ fullName: 'Alignement Empathique',
657
+ question: 'L\'agent comprend-il les émotions, motivations et difficultés de l\'étudiant ?'
658
+ },
659
+ 'ESR': {
660
+ name: 'ESR',
661
+ fullName: 'Encouragement Sensible au Risque',
662
+ question: 'Les encouragements tiennent-ils compte des risques et difficultés identifiés ?'
663
+ },
664
+ 'SDM': {
665
+ name: 'SDM',
666
+ fullName: 'Support aux Défis Mentaux',
667
+ question: 'L\'agent aide-t-il l\'étudiant à développer sa pensée critique et ses compétences cognitives ?'
668
+ },
669
+ 'SM': {
670
+ name: 'SM',
671
+ fullName: 'Support Métacognitif',
672
+ question: 'L\'agent aide-t-il l\'étudiant à réfléchir sur ses propres stratégies d\'apprentissage ?'
673
+ }
674
+ };
675
+
676
+ let html = '';
677
+
678
+ Object.entries(metricDefinitions).forEach(([code, def]) => {
679
+ const value = metrics[code] || 0;
680
+ const stars = getStarsHTML(value);
681
+
682
+ html += `
683
+ <div class="metric-detail">
684
+ <div style="font-weight:bold;color:#1976D2;">${def.fullName} (${def.name})</div>
685
+ <div class="stars-container">${stars}</div>
686
+ <div class="metric-question">${def.question}</div>
687
+ <div style="font-size:11px;color:#666;margin-top:3px;">
688
+ Score: ${value}/5
689
+ </div>
690
+ </div>
691
+ `;
692
+ });
693
+
694
+ return html;
695
+ }
696
+
697
+ function getStarsHTML(value) {
698
+ let stars = '';
699
+ const normalizedValue = Math.min(5, Math.max(0, value)); // S'assurer que c'est entre 0 et 5
700
+ for (let i = 1; i <= 5; i++) {
701
+ stars += `<span class="star ${i <= normalizedValue ? '' : 'empty'}">★</span>`;
702
+ }
703
+ return stars;
704
+ }
705
+
706
+ function displayReviewStatus(review) {
707
+ if (!review) {
708
+ return '<small style="color:#666;">Non reviewé</small>';
709
+ }
710
+
711
+ const decisionText = {
712
+ 'approved': '✅ Approuvé',
713
+ 'needs_revision': '⚠️ Révision',
714
+ 'rejected': '❌ Rejeté'
715
+ };
716
+
717
+ const decisionClass = {
718
+ 'approved': 'review-approved',
719
+ 'needs_revision': 'review-revision',
720
+ 'rejected': 'review-rejected'
721
+ };
722
+
723
+ return `
724
+ <div>
725
+ <span class="review-badge ${decisionClass[review.decision]}">
726
+ ${decisionText[review.decision]}
727
+ </span><br>
728
+ <small style="font-size:10px;">${formatDate(new Date(review.timestamp))}</small><br>
729
+ <small style="color:#666;font-size:10px;">${review.reviewer || 'Anonyme'}</small>
730
+ </div>
731
+ `;
732
+ }
733
+
734
+ // ================= GESTION DES REVIEWS =================
735
+ function openReviewModal(evaluationId) {
736
+ const evaluation = allEvaluations.find(e => e.id === evaluationId);
737
+ if (!evaluation) {
738
+ alert('Évaluation non trouvée');
739
+ return;
740
+ }
741
+
742
+ const scenario = scenariosData.find(s => s.id === evaluation.scenario_id);
743
+ currentReviewEvalId = evaluationId;
744
+
745
+ document.getElementById('modalEvaluator').textContent =
746
+ `${evaluation.evaluator_name || 'Anonyme'} (${evaluation.evaluator_role || ''})`;
747
+ document.getElementById('modalScenario').textContent =
748
+ `Scénario ${evaluation.scenario_id || 'N/A'} - ${scenario ? scenario.title : 'Non trouvé'}`;
749
+ document.getElementById('modalScore').textContent = `${evaluation.trust_score || 0}%`;
750
+
751
+ // Charger le review existant s'il existe
752
+ const existingReview = reviewsData.find(r => r.evaluation_id === evaluationId);
753
+ if (existingReview) {
754
+ document.getElementById('reviewDecision').value = existingReview.decision;
755
+ document.getElementById('reviewComment').value = existingReview.review_comment || '';
756
+ } else {
757
+ document.getElementById('reviewDecision').value = 'approved';
758
+ document.getElementById('reviewComment').value = '';
759
+ }
760
+
761
+ document.getElementById('reviewModal').style.display = 'flex';
762
+ }
763
+
764
+ function closeReviewModal() {
765
+ document.getElementById('reviewModal').style.display = 'none';
766
+ currentReviewEvalId = null;
767
+ }
768
+
769
+ function saveReview() {
770
+ if (!currentReviewEvalId) return;
771
+
772
+ const decision = document.getElementById('reviewDecision').value;
773
+ const reviewComment = document.getElementById('reviewComment').value.trim();
774
+
775
+ const evaluation = allEvaluations.find(e => e.id === currentReviewEvalId);
776
+ if (!evaluation) return;
777
+
778
+ // Créer l'objet review
779
+ const reviewData = {
780
+ evaluation_id: currentReviewEvalId,
781
+ scenario_id: evaluation.scenario_id,
782
+ evaluator_name: evaluation.evaluator_name,
783
+ evaluator_role: evaluation.evaluator_role,
784
+ original_score: evaluation.trust_score,
785
+ original_comment: evaluation.comment,
786
+ decision: decision,
787
+ review_comment: reviewComment,
788
+ reviewer: 'Expert Reviewer',
789
+ timestamp: new Date().toISOString()
790
+ };
791
+
792
+ // Envoyer au serveur via API
793
+ fetch('/api/save_review', {
794
+ method: 'POST',
795
+ headers: {
796
+ 'Content-Type': 'application/json',
797
+ },
798
+ body: JSON.stringify(reviewData)
799
+ })
800
+ .then(response => {
801
+ // Vérifier si la réponse est en HTML (erreur) au lieu de JSON
802
+ const contentType = response.headers.get("content-type");
803
+ if (!contentType || !contentType.includes("application/json")) {
804
+ return response.text().then(text => {
805
+ throw new Error(`Le serveur a renvoyé du HTML au lieu du JSON. Vérifiez que l'API /api/save_review existe.`);
806
+ });
807
+ }
808
+ return response.json();
809
+ })
810
+ .then(data => {
811
+ if (data.status === 'ok') {
812
+ // Ajouter le review localement
813
+ const reviewId = `review_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
814
+ reviewsData.push({
815
+ id: reviewId,
816
+ ...reviewData
817
+ });
818
+
819
+ // Mettre à jour l'affichage
820
+ filterAllEvaluations();
821
+
822
+ // Fermer la modale
823
+ closeReviewModal();
824
+
825
+ showSuccessMessage(`✅ Review sauvegardé avec succès ! (${reviewsData.length} reviews au total)`);
826
+ } else {
827
+ throw new Error(data.error || 'Erreur inconnue');
828
+ }
829
+ })
830
+ .catch(error => {
831
+ console.error('Erreur sauvegarde review:', error);
832
+
833
+ // Sauvegarde locale en cas d'erreur API
834
+ const reviewId = `review_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
835
+ const localReview = {
836
+ id: reviewId,
837
+ ...reviewData
838
+ };
839
+
840
+ reviewsData.push(localReview);
841
+
842
+ // Mettre à jour l'affichage
843
+ filterAllEvaluations();
844
+
845
+ // Fermer la modale
846
+ closeReviewModal();
847
+
848
+ showSuccessMessage(`✅ Review sauvegardé localement (API non disponible). (${reviewsData.length} reviews)`);
849
+
850
+ // Afficher un avertissement
851
+ showWarningMessage(`Le review a été sauvegardé localement. Pour une sauvegarde permanente,
852
+ ajoutez l'endpoint /api/save_review à votre backend Flask.`);
853
+ });
854
+ }
855
+
856
+ // ================= GRAPHIQUES =================
857
+ function updateCharts(scenarioMetrics) {
858
+ // Détruire les anciens graphiques s'ils existent
859
+ if (chart1) chart1.destroy();
860
+ if (chart2) chart2.destroy();
861
+
862
+ if (scenarioMetrics.length === 0) {
863
+ document.querySelectorAll('.chart-container').forEach(container => {
864
+ container.innerHTML = '<p style="text-align:center;color:#666;padding:40px;">Aucune donnée à afficher</p>';
865
+ });
866
+ return;
867
+ }
868
+
869
+ // Données pour les graphiques
870
+ const labels = scenarioMetrics.map(m => `Scénario ${m.scenarioId}`);
871
+ const avgScores = scenarioMetrics.map(m => Math.round(m.avgScore));
872
+ const stdDevs = scenarioMetrics.map(m => Math.round(m.stdDev));
873
+ const evaluationCounts = scenarioMetrics.map(m => m.evaluations.length);
874
+
875
+ // Graphique 1 : Scores moyens par scénario
876
+ const ctx1 = document.getElementById('confidenceChart').getContext('2d');
877
+ chart1 = new Chart(ctx1, {
878
+ type: 'bar',
879
+ data: {
880
+ labels: labels,
881
+ datasets: [{
882
+ label: 'Confiance moyenne (%)',
883
+ data: avgScores,
884
+ backgroundColor: avgScores.map(score =>
885
+ score >= 70 ? 'rgba(67, 160, 71, 0.7)' :
886
+ score >= 40 ? 'rgba(251, 140, 0, 0.7)' :
887
+ 'rgba(229, 57, 53, 0.7)'
888
+ ),
889
+ borderColor: avgScores.map(score =>
890
+ score >= 70 ? '#43A047' :
891
+ score >= 40 ? '#FB8C00' :
892
+ '#E53935'
893
+ ),
894
+ borderWidth: 1
895
+ }]
896
+ },
897
+ options: {
898
+ responsive: true,
899
+ maintainAspectRatio: false,
900
+ plugins: {
901
+ title: {
902
+ display: true,
903
+ text: 'Confiance moyenne par scénario',
904
+ font: { size: 16 }
905
+ },
906
+ tooltip: {
907
+ callbacks: {
908
+ label: function(context) {
909
+ const scenario = scenarioMetrics[context.dataIndex];
910
+ return [
911
+ `Confiance: ${context.parsed.y}%`,
912
+ `Évaluations: ${scenario.evaluations.length}`,
913
+ `Min: ${Math.round(scenario.minScore)}%, Max: ${Math.round(scenario.maxScore)}%`,
914
+ `Conflit: ${scenario.conflictLevel === 'high' ? 'Élevé' :
915
+ scenario.conflictLevel === 'medium' ? 'Moyen' : 'Faible'}`
916
+ ];
917
+ }
918
+ }
919
+ }
920
+ },
921
+ scales: {
922
+ y: {
923
+ beginAtZero: true,
924
+ max: 100,
925
+ title: {
926
+ display: true,
927
+ text: 'Score de confiance (%)'
928
+ }
929
+ }
930
+ }
931
+ }
932
+ });
933
+
934
+ // Graphique 2 : Niveau de conflit
935
+ const ctx2 = document.getElementById('conflictChart').getContext('2d');
936
+ chart2 = new Chart(ctx2, {
937
+ type: 'line',
938
+ data: {
939
+ labels: labels,
940
+ datasets: [
941
+ {
942
+ label: 'Écart-type (conflit)',
943
+ data: stdDevs,
944
+ borderColor: '#E53935',
945
+ backgroundColor: 'rgba(229, 57, 53, 0.1)',
946
+ borderWidth: 2,
947
+ fill: true,
948
+ tension: 0.4
949
+ },
950
+ {
951
+ label: 'Nombre d\'évaluations',
952
+ data: evaluationCounts,
953
+ borderColor: '#1976D2',
954
+ backgroundColor: 'rgba(25, 118, 210, 0.1)',
955
+ borderWidth: 2,
956
+ fill: true,
957
+ tension: 0.4,
958
+ yAxisID: 'y1'
959
+ }
960
+ ]
961
+ },
962
+ options: {
963
+ responsive: true,
964
+ maintainAspectRatio: false,
965
+ plugins: {
966
+ title: {
967
+ display: true,
968
+ text: 'Niveau de conflit et nombre d\'évaluations',
969
+ font: { size: 16 }
970
+ }
971
+ },
972
+ scales: {
973
+ y: {
974
+ beginAtZero: true,
975
+ title: {
976
+ display: true,
977
+ text: 'Écart-type (%)'
978
+ }
979
+ },
980
+ y1: {
981
+ beginAtZero: true,
982
+ position: 'right',
983
+ title: {
984
+ display: true,
985
+ text: 'Nombre d\'évaluations'
986
+ },
987
+ grid: {
988
+ drawOnChartArea: false
989
+ }
990
+ }
991
+ }
992
+ }
993
+ });
994
+ }
995
+
996
+ // ================= DÉTAILS D'UN SCÉNARIO =================
997
+ function showScenarioDetails(scenarioId) {
998
+ const scenario = scenariosData.find(s => s.id === scenarioId);
999
+ const evaluations = allEvaluations.filter(e => e.scenario_id === scenarioId);
1000
+
1001
+ if (!scenario) {
1002
+ alert('Scénario non trouvé');
1003
+ return;
1004
+ }
1005
+
1006
+ // Mettre à jour le titre
1007
+ document.getElementById('detailScenarioTitle').textContent = `${scenarioId} - ${scenario.title}`;
1008
+
1009
+ // Calculer les métriques détaillées
1010
+ const avgScore = evaluations.reduce((sum, e) => sum + (e.trust_score || 0), 0) / evaluations.length;
1011
+ const metricsAvg = {};
1012
+ const metricsStdDev = {};
1013
+
1014
+ // Initialiser les moyennes
1015
+ const metricKeys = ['AR', 'AE', 'ESR', 'SDM', 'SM'];
1016
+ metricKeys.forEach(key => {
1017
+ const values = evaluations.map(e => e.trust_metrics[key] || 0);
1018
+ metricsAvg[key] = values.reduce((sum, v) => sum + v, 0) / values.length;
1019
+
1020
+ // Calculer l'écart-type
1021
+ if (values.length > 1) {
1022
+ const variance = values.reduce((sum, v) => sum + Math.pow(v - metricsAvg[key], 2), 0) / values.length;
1023
+ metricsStdDev[key] = Math.sqrt(variance);
1024
+ } else {
1025
+ metricsStdDev[key] = 0;
1026
+ }
1027
+ });
1028
+
1029
+ // Préparer le contenu HTML
1030
+ let html = `
1031
+ <div style="margin-bottom: 20px;">
1032
+ <h3>${scenario.manifest?.manifeste || 'Pas de manifeste'}</h3>
1033
+ <p><strong>Type:</strong> ${scenario.manifest?.type || 'Non spécifié'}</p>
1034
+ <p><strong>Description:</strong> ${scenario.manifest?.description || 'Pas de description'}</p>
1035
+ </div>
1036
+
1037
+ <div class="details-grid">
1038
+ <div class="details-card">
1039
+ <h4>📊 Résumé des évaluations</h4>
1040
+ <p><strong>Nombre d'évaluations:</strong> ${evaluations.length}</p>
1041
+ <p><strong>Confiance moyenne:</strong> ${Math.round(avgScore)}%</p>
1042
+ <p><strong>Plage de scores:</strong> ${Math.round(Math.min(...evaluations.map(e => e.trust_score || 0)))}% -
1043
+ ${Math.round(Math.max(...evaluations.map(e => e.trust_score || 0)))}%</p>
1044
+ <p><strong>Période:</strong> ${formatDate(new Date(Math.min(...evaluations.map(e => new Date(e.timestamp).getTime()))))}
1045
+ à ${formatDate(new Date(Math.max(...evaluations.map(e => new Date(e.timestamp).getTime()))))}</p>
1046
+ </div>
1047
+
1048
+ <div class="details-card">
1049
+ <h4>🎯 Métriques moyennes</h4>
1050
+ ${metricKeys.map(key => `
1051
+ <div style="margin: 5px 0;">
1052
+ <span style="display:inline-block;width:100px;">${getMetricName(key)}:</span>
1053
+ <span style="font-weight:bold;color:#1976D2;">${metricsAvg[key].toFixed(1)}/5</span>
1054
+ <div style="display:inline-block;width:100px;height:10px;background:#e0e0e0;border-radius:5px;margin-left:10px;">
1055
+ <div style="height:100%;width:${metricsAvg[key] * 20}%;background:#1976D2;border-radius:5px;"></div>
1056
+ </div>
1057
+ </div>
1058
+ `).join('')}
1059
+ </div>
1060
+ </div>
1061
+
1062
+ <h3 style="margin-top:30px;">📋 Détail des évaluations</h3>
1063
+ `;
1064
+
1065
+ // Tableau des évaluations individuelles
1066
+ html += `
1067
+ <table style="margin-top:20px;">
1068
+ <thead>
1069
+ <tr>
1070
+ <th>Évaluateur</th>
1071
+ <th>Date</th>
1072
+ <th>Score global</th>
1073
+ ${metricKeys.map(key => `<th>${getMetricName(key)}</th>`).join('')}
1074
+ <th>Commentaire</th>
1075
+ </tr>
1076
+ </thead>
1077
+ <tbody>
1078
+ `;
1079
+
1080
+ evaluations.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)).forEach(eval => {
1081
+ html += `
1082
+ <tr>
1083
+ <td><strong>${eval.evaluator_name || 'Anonyme'}</strong><br>
1084
+ <small>${eval.evaluator_role || ''}</small></td>
1085
+ <td>${formatDate(new Date(eval.timestamp))}</td>
1086
+ <td>
1087
+ <div style="font-weight:bold;color:#1976D2;">${eval.trust_score || 0}%</div>
1088
+ <div class="confidence-bar">
1089
+ <div class="confidence-fill ${getConfidenceClass(eval.trust_score || 0)}"
1090
+ style="width: ${eval.trust_score || 0}%"></div>
1091
+ </div>
1092
+ </td>
1093
+ ${metricKeys.map(key => `
1094
+ <td>
1095
+ <div style="text-align:center;font-weight:bold;color:#1976D2;">${eval.trust_metrics[key] || 0}/5</div>
1096
+ <div style="display:flex;justify-content:center;gap:2px;margin-top:5px;">
1097
+ ${[1,2,3,4,5].map(i => `
1098
+ <div style="width:12px;height:12px;border-radius:50%;
1099
+ background:${i <= (eval.trust_metrics[key] || 0) ? '#1976D2' : '#e0e0e0'}"></div>
1100
+ `).join('')}
1101
+ </div>
1102
+ </td>
1103
+ `).join('')}
1104
+ <td><small style="color:#666;">${eval.comment || 'Aucun commentaire'}</small></td>
1105
+ </tr>
1106
+ `;
1107
+ });
1108
+
1109
+ html += `</tbody></table>`;
1110
+
1111
+ // Analyse des divergences
1112
+ if (evaluations.length > 1) {
1113
+ html += `
1114
+ <h3 style="margin-top:30px;">🔍 Analyse des divergences</h3>
1115
+ <div class="comparison-chart">
1116
+ `;
1117
+
1118
+ metricKeys.forEach(key => {
1119
+ const values = evaluations.map(e => e.trust_metrics[key] || 0);
1120
+ const maxDiff = Math.max(...values) - Math.min(...values);
1121
+
1122
+ html += `
1123
+ <div class="comparison-item">
1124
+ <h4>${getMetricName(key)}</h4>
1125
+ <p style="font-size:24px;font-weight:bold;color:${maxDiff >= 3 ? '#E53935' :
1126
+ maxDiff >= 2 ? '#FB8C00' : '#43A047'}">
1127
+ ${maxDiff} pts
1128
+ </p>
1129
+ <p><small>Différence maximale</small></p>
1130
+ <p>Moyenne: ${metricsAvg[key].toFixed(1)}/5</p>
1131
+ <p>Écart-type: ${metricsStdDev[key].toFixed(1)}</p>
1132
+ </div>
1133
+ `;
1134
+ });
1135
+
1136
+ html += `</div>`;
1137
+ }
1138
+
1139
+ // Mettre à jour le contenu et afficher
1140
+ document.getElementById('scenarioContent').innerHTML = html;
1141
+ document.getElementById('scenarioDetails').classList.add('active');
1142
+
1143
+ // Scroll vers les détails
1144
+ document.getElementById('scenarioDetails').scrollIntoView({ behavior: 'smooth' });
1145
+ }
1146
+
1147
+ // ================= FONCTIONS UTILITAIRES =================
1148
+ function getMetricName(code) {
1149
+ const names = {
1150
+ 'AR': 'Authenticité Réflexive',
1151
+ 'AE': 'Alignement Empathique',
1152
+ 'ESR': 'Encouragement Sensible au Risque',
1153
+ 'SDM': 'Support aux Défis Mentaux',
1154
+ 'SM': 'Support Métacognitif'
1155
+ };
1156
+ return names[code] || code;
1157
+ }
1158
+
1159
+ function getConfidenceClass(score) {
1160
+ if (score >= 70) return 'high-confidence';
1161
+ if (score >= 40) return 'medium-confidence';
1162
+ return 'low-confidence';
1163
+ }
1164
+
1165
+ function formatDate(date) {
1166
+ if (!date || isNaN(date.getTime())) {
1167
+ return 'Date invalide';
1168
+ }
1169
+ return date.toLocaleDateString('fr-FR', {
1170
+ day: '2-digit',
1171
+ month: '2-digit',
1172
+ year: 'numeric',
1173
+ hour: '2-digit',
1174
+ minute: '2-digit'
1175
+ });
1176
+ }
1177
+
1178
+ // ================= GESTION DES MESSAGES =================
1179
+ function showLoadingMessage(message) {
1180
+ const loadingDiv = document.querySelector('.loading p') || document.createElement('p');
1181
+ loadingDiv.textContent = message;
1182
+ }
1183
+
1184
+ function showSuccessMessage(message) {
1185
+ const successDiv = document.getElementById('successMessage');
1186
+ if (successDiv) {
1187
+ successDiv.innerHTML = `✅ ${message}`;
1188
+ successDiv.style.display = 'block';
1189
+ setTimeout(() => {
1190
+ successDiv.style.display = 'none';
1191
+ }, 5000);
1192
+ }
1193
+ }
1194
+
1195
+ function showErrorMessage(message) {
1196
+ const tableContainer = document.getElementById('evaluationsTable');
1197
+ if (tableContainer) {
1198
+ tableContainer.innerHTML = `
1199
+ <div class="error-message">
1200
+ <h3>⚠️ Erreur de chargement</h3>
1201
+ <div>${message}</div>
1202
+ </div>
1203
+ `;
1204
+ }
1205
+ }
1206
+
1207
+ function showWarningMessage(message) {
1208
+ const warningDiv = document.getElementById('warningMessage');
1209
+ if (warningDiv) {
1210
+ warningDiv.innerHTML = `
1211
+ <strong>⚠️ Aucune donnée trouvée</strong>
1212
+ <p>${message}</p>
1213
+ <button onclick="generateTestData()" class="test-data-btn">
1214
+ 🧪 Générer des données de test
1215
+ </button>
1216
+ `;
1217
+ warningDiv.style.display = 'block';
1218
+ }
1219
+ }
1220
+
1221
+ // ================= EXPOSITION DES FONCTIONS GLOBALES =================
1222
+ window.filterData = filterData;
1223
+ window.filterAllEvaluations = filterAllEvaluations;
1224
+ window.showScenarioDetails = showScenarioDetails;
1225
+ window.openReviewModal = openReviewModal;
1226
+ window.closeReviewModal = closeReviewModal;
1227
+ window.saveReview = saveReview;
1228
+ window.generateTestData = generateTestData;
1229
+ window.loadData = loadData;