ReflAgent / cross_reading /consensus_building.html
OUAREDAEK's picture
Upload cross_reading/consensus_building.html with huggingface_hub
dca59c9 verified
Raw
History Blame Contribute Delete
81.4 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Système de Calibration Expert - ReflAgent</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
color: #333;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
border: 1px solid #dee2e6;
}
/* Onglets */
.tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 2px solid #dee2e6;
padding-bottom: 0;
}
.tab-button {
padding: 15px 30px;
background: #f8f9fa;
border: none;
border-radius: 10px 10px 0 0;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
color: #6c757d;
font-size: 16px;
}
.tab-button.active {
background: #3498db;
color: white;
box-shadow: 0 4px 10px rgba(52, 152, 219, 0.3);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Partie 1 : Interface Expert */
.evaluation-container {
max-width: 900px;
margin: 0 auto;
}
.scenario-selector {
background: #f8f9fa;
padding: 25px;
border-radius: 15px;
margin-bottom: 30px;
border-left: 5px solid #3498db;
}
.scenario-selector select {
width: 100%;
padding: 15px;
border: 2px solid #dee2e6;
border-radius: 10px;
font-size: 16px;
margin-top: 10px;
background: white;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
}
.metric-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
border: 1px solid #e9ecef;
}
.metric-slider {
width: 100%;
margin: 15px 0;
-webkit-appearance: none;
height: 8px;
background: #e9ecef;
border-radius: 4px;
outline: none;
}
.metric-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #3498db;
cursor: pointer;
border: 3px solid white;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.slider-value {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
margin-top: 10px;
}
.metric-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
margin-top: 10px;
}
.badge-low {
background: #f8d7da;
color: #721c24;
}
.badge-medium {
background: #fff3cd;
color: #856404;
}
.badge-high {
background: #d4edda;
color: #155724;
}
.calibrate-btn {
display: block;
width: 200px;
margin: 40px auto;
padding: 15px 30px;
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
border: none;
border-radius: 10px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.calibrate-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(39, 174, 96, 0.3);
}
/* Partie 2 : Dashboard Consensus */
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
transition: transform 0.3s;
border-top: 4px solid #3498db;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card.highlight {
border-top-color: #e74c3c;
background: linear-gradient(135deg, #fff5f5, #fff);
}
.stat-value {
font-size: 42px;
font-weight: bold;
color: #2c3e50;
margin: 10px 0;
}
.chart-container {
background: white;
border-radius: 15px;
padding: 25px;
margin: 30px 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
border: 1px solid #e9ecef;
}
.chart-wrapper {
height: 500px;
position: relative;
margin: 20px 0;
}
.section-title {
color: #2c3e50;
margin: 40px 0 20px;
padding-bottom: 10px;
border-bottom: 2px solid #4a90e2;
display: flex;
align-items: center;
gap: 10px;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
margin: 25px 0;
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0,0,0,0.05);
}
.comparison-table th {
background: linear-gradient(135deg, #2c3e50, #34495e);
color: white;
padding: 15px;
text-align: left;
font-weight: 600;
}
.comparison-table td {
padding: 15px;
border-bottom: 1px solid #e9ecef;
}
.metric-details {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.metric-bar {
height: 8px;
background: #e9ecef;
border-radius: 4px;
margin: 5px 0;
overflow: hidden;
}
.metric-fill {
height: 100%;
border-radius: 4px;
}
.loading {
text-align: center;
padding: 60px;
color: #7f8c8d;
}
.loading-spinner {
display: inline-block;
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
animation: slideIn 0.3s ease;
font-weight: bold;
}
.notification.success {
background: #27ae60;
color: white;
}
.notification.error {
background: #e74c3c;
color: white;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
/* Section Consensus Building */
.consensus-building {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 15px;
padding: 30px;
margin: 40px 0;
border-left: 5px solid #3498db;
box-shadow: 0 5px 20px rgba(0,0,0,0.05);
}
.consensus-building h2 {
color: #2c3e50;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.consensus-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
}
.consensus-text {
line-height: 1.6;
color: #555;
}
.consensus-text p {
margin-bottom: 15px;
}
.kappa-metrics {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
}
.kappa-metrics h4 {
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.kappa-value {
display: inline-block;
padding: 10px 20px;
background: #2c3e50;
color: white;
border-radius: 20px;
font-weight: bold;
font-size: 24px;
margin: 10px 0;
}
.kappa-stage {
margin: 15px 0;
padding: 12px;
border-left: 4px solid;
background: #f8f9fa;
border-radius: 0 5px 5px 0;
}
.kappa-stage.annotation {
border-left-color: #3498db;
}
.kappa-stage.revision {
border-left-color: #2ecc71;
}
.kappa-stage.evaluation {
border-left-color: #e74c3c;
}
.expert-process {
background: #f8f9fa;
padding: 25px;
border-radius: 15px;
margin: 30px 0;
border-left: 5px solid #3498db;
}
.expert-process h3 {
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.expert-process ul {
list-style-type: none;
padding-left: 20px;
}
.expert-process li {
margin-bottom: 12px;
padding-left: 25px;
position: relative;
}
.expert-process li:before {
content: "✓";
position: absolute;
left: 0;
color: #27ae60;
font-weight: bold;
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
.tabs {
flex-direction: column;
}
.tab-button {
width: 100%;
border-radius: 10px;
margin-bottom: 5px;
}
.chart-wrapper {
height: 400px;
}
.metrics-grid {
grid-template-columns: 1fr;
}
.consensus-content {
grid-template-columns: 1fr;
}
}
/* Styles pour les messages d'erreur/absence de données */
.no-data-message {
text-align: center;
padding: 60px;
color: #7f8c8d;
background: #f8f9fa;
border-radius: 10px;
margin: 20px 0;
}
.no-data-message h3 {
margin-bottom: 15px;
color: #2c3e50;
}
.no-data-message button {
margin-top: 15px;
padding: 10px 20px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s;
}
.no-data-message button:hover {
background: #2980b9;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container">
<div class="tabs">
<button class="tab-button active" data-tab="expert">👨‍🔬 Évaluation Expert</button>
<button class="tab-button" data-tab="consensus">📊 Dashboard Consensus</button>
</div>
<!-- Partie 1 : Interface Expert -->
<div id="expert" class="tab-content active">
<div class="evaluation-container">
<header>
<h1 style="color: #2c3e50; margin-bottom: 20px;">👨‍🔬 Calibration Expert</h1>
<p class="subtitle" style="color: #7f8c8d; margin-bottom: 30px;">
Évaluez les 5 métriques de confiance pour chaque scénario. L'<strong>Indice Global de Confiance</strong>
est automatiquement calculé comme la moyenne des 5 métriques.
</p>
</header>
<div class="scenario-selector">
<h3>📋 Sélection du Scénario</h3>
<select id="scenarioSelect">
<option value="">Chargement des scénarios...</option>
</select>
<p id="scenarioDescription" style="margin-top: 15px; color: #666; font-style: italic;"></p>
</div>
<div id="metricsEvaluation" style="display: none;">
<h3 style="color: #2c3e50; margin-bottom: 20px;">📊 Évaluation des 5 Métriques de Confiance</h3>
<div class="metrics-grid" id="metricsGrid">
<!-- Les 5 métriques seront générées ici -->
</div>
<!-- Indice Global de Confiance -->
<div id="globalIndex" style="margin-top: 30px; padding: 25px; background: linear-gradient(135deg, #f8f9fa, #e9ecef); border-radius: 15px; border-left: 5px solid #9b59b6;">
<h4 style="color: #2c3e50; margin-bottom: 10px;">🎯 Indice Global de Confiance</h4>
<p style="color: #7f8c8d; margin-bottom: 15px;">
Moyenne automatique des 5 métriques évaluées ci-dessus
</p>
<div style="display: flex; align-items: center; gap: 20px;">
<div style="font-size: 48px; font-weight: bold; color: #9b59b6;" id="globalIndexValue">0%</div>
<div style="flex: 1;">
<div style="height: 20px; background: #e9ecef; border-radius: 10px; overflow: hidden;">
<div id="globalIndexBar" style="height: 100%; background: linear-gradient(90deg, #9b59b6, #8e44ad); width: 0%; transition: width 0.5s;"></div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 12px; color: #7f8c8d;">
<span>0%</span>
<span>50%</span>
<span>100%</span>
</div>
</div>
</div>
<div id="globalIndexBadge" class="metric-badge" style="margin-top: 15px;">
Indice non calculé
</div>
</div>
<button class="calibrate-btn" onclick="saveCalibration()">
✅ Calibrer ce Scénario
</button>
<div id="previousEvaluation" style="margin-top: 40px; padding: 20px; background: #f8f9fa; border-radius: 10px; display: none;">
<h4 style="color: #2c3e50; margin-bottom: 15px;">📝 Évaluation précédente</h4>
<div id="previousMetrics"></div>
</div>
</div>
<div id="loadingExpert" class="loading" style="display: none;">
<div class="loading-spinner"></div>
<p>Enregistrement de l'évaluation...</p>
</div>
</div>
</div>
<!-- Partie 2 : Dashboard Consensus -->
<div id="consensus" class="tab-content">
<a href="javascript:void(0)" onclick="switchTab('expert')" class="tab-button" style="display: inline-block; margin-bottom: 20px; background: linear-gradient(135deg, #3498db, #2980b9); color: white;">⬅️ Retour à l'Évaluation</a>
<header>
<h1>🔄 Établissement du Consensus - Processus Expert</h1>
<p class="subtitle">
<strong>Phase de Validation par les Experts - Établissement du Consensus :</strong>
Suite à l'évaluation individuelle, 14 experts ont participé à un processus d'établissement de consensus
pour traiter la variabilité inter-évaluateurs. Les divergences ont été analysées et résolues lors de sessions
de calibration, aboutissant à un accord inter-codeur substantiel (Kappa de Cohen > 0,75) à toutes les étapes.
</p>
</header>
<!-- Section Consensus Building -->
<div class="consensus-building">
<h2>🎯 Objectif de ce Dashboard</h2>
<div class="consensus-content">
<div class="consensus-text">
<p>Ce tableau de bord présente les résultats du <strong>processus d'établissement de consensus</strong>
mené par 14 experts interdisciplinaires. Il compare les évaluations individuelles initiales
avec les évaluations harmonisées suite aux sessions de calibration, montrant l'amélioration
de la cohérence et de la fiabilité des jugements experts.</p>
<p><strong>Résultat clé :</strong> Accord inter-évaluateurs substantiel (κ > 0,75) atteint pour
l'ensemble des scénarios après le processus de consensus.</p>
<p><strong>Processus :</strong> Suite à la phase d'évaluation individuelle, un processus d'établissement de consensus impliquant 14 experts
a été mené pour traiter la variabilité inter-évaluateurs. Cette phase a transformé les jugements individuels
en une référence collective, garantissant des critères d'évaluation cohérents au sein de l'équipe d'experts interdisciplinaire.</p>
</div>
<div class="kappa-metrics">
<h4>📐 Fiabilité Inter-Codeurs</h4>
<div class="kappa-value">κ > 0,75</div>
<p style="color: #7f8c8d; margin-bottom: 15px;">Accord substantiel entre experts</p>
<div class="kappa-stage annotation">
<strong>Annotation :</strong> κ > 0,78
</div>
<div class="kappa-stage revision">
<strong>Révision :</strong> κ > 0,82
</div>
<div class="kappa-stage evaluation">
<strong>Évaluation :</strong> κ > 0,85
</div>
</div>
</div>
</div>
<!-- Processus d'expert -->
<div class="expert-process">
<h3>🔬 Processus de Validation par les Experts</h3>
<ul>
<li><strong>Phase d'évaluation individuelle :</strong> Chaque expert évalue indépendamment les scénarios</li>
<li><strong>Identification des divergences :</strong> Analyse systématique des évaluations divergentes</li>
<li><strong>Sessions de calibration :</strong> Sessions ciblées pour les évaluations persistemment divergentes</li>
<li><strong>Établissement de critères communs :</strong> Transformation des jugements individuels en référentiel collectif</li>
<li><strong>Validation finale :</strong> Approbation consensuelle des évaluations</li>
</ul>
</div>
<!-- Aperçu des statistiques -->
<div class="stats-overview" id="statsOverview">
<!-- Généré dynamiquement -->
</div>
<!-- Graphique principal -->
<div class="chart-container">
<h3>📊 Indice Global de Confiance: Avant vs Après</h3>
<div class="chart-wrapper" id="mainTrustChartContainer">
<canvas id="mainTrustChart"></canvas>
</div>
</div>
<!-- Graphique détaillé des métriques -->
<div class="chart-container">
<h3>📈 Détail des 5 Métriques (Moyenne)</h3>
<div class="chart-wrapper" id="detailedMetricsChartContainer">
<canvas id="detailedMetricsChart"></canvas>
</div>
</div>
<!-- Graphique d'amélioration -->
<div class="chart-container">
<h3>📈 Amélioration par Scénario</h3>
<div class="chart-wrapper" id="improvementChartContainer">
<canvas id="improvementChart"></canvas>
</div>
</div>
<!-- Détail par métrique et scénario -->
<h2 class="section-title">📋 Détail par Métrique et Scénario</h2>
<div id="detailedComparison">
<!-- Généré dynamiquement -->
</div>
<!-- Zone de chargement -->
<div id="loadingConsensus" class="loading">
<div class="loading-spinner"></div>
<p>Chargement des données de comparaison...</p>
</div>
</div>
</div>
<script>
// ============================================
// DONNÉES ET CONFIGURATION
// ============================================
// Les 5 métriques spécifiques avec leurs acronymes et descriptions
const metrics = [
{
id: 'AR',
name: 'Authenticité Réflexive (AR)',
description: 'Capacité du système à refléter une compréhension authentique et contextualisée',
color: '#3498db'
},
{
id: 'AE',
name: 'Alignement Empathique (AE)',
description: 'Adéquation entre les réponses du système et l\'état émotionnel/utilisateur',
color: '#2ecc71'
},
{
id: 'ESR',
name: 'Encouragement Sensible au Risque (ESR)',
description: 'Capacité à encourager la prise de risque calculée tout en indiquant les limites',
color: '#e74c3c'
},
{
id: 'SDM',
name: 'Support aux Défis Mentaux (SDM)',
description: 'Soutien dans la résolution de problèmes complexes et défis cognitifs',
color: '#f39c12'
},
{
id: 'SM',
name: 'Support Métacognitif (SM)',
description: 'Aide à la réflexion sur ses propres processus de pensée et d\'apprentissage',
color: '#9b59b6'
}
];
// Scénarios - seront chargés depuis scenarios_list.json
let scenariosList = [];
// Données initiales de confiance (chargées depuis trust_stats.json)
let trustStatsData = {};
// Données calibrées (depuis localStorage et calibration-trust.json)
let calibrationData = JSON.parse(localStorage.getItem('calibration-trust')) || {};
// Variables pour stocker les instances de graphiques
let mainChart = null;
let improvementChart = null;
let detailedChart = null;
// ============================================
// CHARGEMENT DES SCÉNARIOS DEPUIS JSON
// ============================================
async function loadScenariosFromJSON() {
try {
const response = await fetch('scenarios_list.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Vérifier si c'est un tableau ou un objet avec une propriété
if (Array.isArray(data)) {
scenariosList = data;
console.log(`${scenariosList.length} scénarios chargés depuis scenarios_list.json (format tableau)`);
} else if (data.scenarios && Array.isArray(data.scenarios)) {
scenariosList = data.scenarios;
console.log(`${scenariosList.length} scénarios chargés depuis scenarios_list.json (format objet.scenarios)`);
} else {
throw new Error('Format de fichier JSON invalide');
}
// S'assurer que tous les scénarios ont les bons IDs
scenariosList.forEach((scenario, index) => {
if (!scenario.id) {
scenario.id = `S${index + 1}`;
}
// Normaliser l'ID au format S1, S2, etc.
if (typeof scenario.id === 'number') {
scenario.id = `S${scenario.id}`;
}
});
} catch (error) {
console.error('Erreur de chargement de scenarios_list.json:', error);
// Générer des scénarios par défaut
scenariosList = generateDefaultScenarios();
console.log(`Utilisation de ${scenariosList.length} scénarios par défaut`);
}
}
function generateDefaultScenarios() {
// Générer des scénarios par défaut S1 à S16
const scenarios = [];
for (let i = 1; i <= 16; i++) {
scenarios.push({
id: `S${i}`,
name: `Scénario ${i}`,
description: `Description du scénario ${i}`
});
}
return scenarios;
}
// ============================================
// CHARGEMENT DES DONNÉES RÉELLES DES FICHIERS JSON
// ============================================
async function loadRealDataFromJSON() {
try {
// 1. Charger les données de trust_stats.json pour les valeurs initiales
const trustResponse = await fetch('trust_stats.json');
if (trustResponse.ok) {
const trustData = await trustResponse.json();
console.log('Données chargées depuis trust_stats.json:', trustData);
// Convertir les données de trust_stats.json au bon format
if (trustData.evaluations && Array.isArray(trustData.evaluations)) {
trustStatsData = {};
trustData.evaluations.forEach(eval => {
const scenarioId = `S${eval.scenario_id}`;
// Normaliser les métriques (RA -> AR, EA -> AE, etc.)
const metricsData = eval.trust_metrics || {};
const normalizedMetrics = {
AR: (metricsData.RA || metricsData.AR || 0) * 20, // Convertir 0-5 à 0-100
AE: (metricsData.EA || metricsData.AE || 0) * 20,
ESR: (metricsData.RE || metricsData.ESR || 0) * 20,
SDM: (metricsData.MCS || metricsData.SDM || 0) * 20,
SM: (metricsData.MS || metricsData.SM || 0) * 20
};
// Calculer la moyenne
const values = Object.values(normalizedMetrics);
const moyenne = values.reduce((a, b) => a + b, 0) / values.length;
trustStatsData[scenarioId] = {
...normalizedMetrics,
moyenne: moyenne
};
});
console.log('trustStatsData mis à jour:', trustStatsData);
}
}
// 2. Charger les données de calibration-trust.json
const calibrationResponse = await fetch('calibration-trust.json');
if (calibrationResponse.ok) {
const calibrationJson = await calibrationResponse.json();
console.log('Données chargées depuis calibration-trust.json:', calibrationJson);
// Extraire les évaluations de tous les experts
if (calibrationJson.experts && Array.isArray(calibrationJson.experts)) {
calibrationJson.experts.forEach(expert => {
if (expert.evaluations && Array.isArray(expert.evaluations)) {
expert.evaluations.forEach(evaluation => {
if (evaluation.scenario) {
// Stocker la dernière évaluation pour chaque scénario
calibrationData[evaluation.scenario] = evaluation;
}
});
}
});
}
console.log('calibrationData chargé depuis JSON:', calibrationData);
// Mettre à jour localStorage avec les données du fichier
localStorage.setItem('calibration-trust', JSON.stringify(calibrationData));
}
// 3. S'assurer que tous les scénarios S1-S16 ont des données par défaut
for (let i = 1; i <= 16; i++) {
const scenarioId = `S${i}`;
if (!trustStatsData[scenarioId]) {
trustStatsData[scenarioId] = {
AR: 50, AE: 50, ESR: 50, SDM: 50, SM: 50, moyenne: 50
};
}
}
} catch (error) {
console.error('Erreur de chargement des données JSON:', error);
// Initialiser avec des données par défaut
for (let i = 1; i <= 16; i++) {
const scenarioId = `S${i}`;
trustStatsData[scenarioId] = {
AR: 50, AE: 50, ESR: 50, SDM: 50, SM: 50, moyenne: 50
};
}
}
}
// ============================================
// FONCTIONS DE GESTION DES ONGLETS
// ============================================
function switchTab(tabName) {
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(tabName).classList.add('active');
if (tabName === 'consensus') {
loadConsensusData();
}
}
// ============================================
// PARTIE 1 : INTERFACE EXPERT
// ============================================
async function loadScenarios() {
const select = document.getElementById('scenarioSelect');
select.innerHTML = '<option value="">Chargement des scénarios...</option>';
// Charger les scénarios depuis le fichier JSON
await loadScenariosFromJSON();
// Remplir le select
select.innerHTML = '<option value="">Sélectionnez un scénario...</option>';
scenariosList.forEach(scenario => {
const option = document.createElement('option');
option.value = scenario.id;
option.textContent = `${scenario.id}: ${scenario.name}`;
select.appendChild(option);
});
// Ajouter un écouteur pour le changement de scénario
select.addEventListener('change', function() {
const selectedId = this.value;
const scenario = scenariosList.find(s => s.id === selectedId);
if (scenario) {
document.getElementById('scenarioDescription').textContent = scenario.description || 'Aucune description disponible';
document.getElementById('metricsEvaluation').style.display = 'block';
loadMetricsForScenario(selectedId);
} else {
document.getElementById('metricsEvaluation').style.display = 'none';
document.getElementById('scenarioDescription').textContent = 'Scénario non trouvé';
}
});
// Sélectionner le premier scénario par défaut
if (scenariosList.length > 0) {
setTimeout(() => {
select.value = scenariosList[0].id;
select.dispatchEvent(new Event('change'));
}, 500);
}
}
function loadMetricsForScenario(scenarioId) {
const metricsGrid = document.getElementById('metricsGrid');
metricsGrid.innerHTML = '';
// S'assurer que l'ID est au bon format
const normalizedId = typeof scenarioId === 'number' ? `S${scenarioId}` : scenarioId;
const previousCalibration = calibrationData[normalizedId];
const initialData = trustStatsData[normalizedId];
metrics.forEach(metric => {
const initialValue = initialData ? initialData[metric.id] : 50;
const previousValue = previousCalibration ? previousCalibration[metric.id] : null;
const currentValue = previousValue !== null && previousValue !== undefined ? previousValue : initialValue;
const metricCard = document.createElement('div');
metricCard.className = 'metric-card';
metricCard.style.borderTop = `4px solid ${metric.color}`;
metricCard.innerHTML = `
<h4>${metric.name}</h4>
<p style="font-size: 14px; color: #666; margin: 10px 0;">${metric.description}</p>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span style="color: #e74c3c; font-weight: bold;">
Initial: ${initialValue}%
</span>
${previousValue !== null && previousValue !== undefined ?
`<span style="color: #27ae60; font-weight: bold;">
Précédent: ${previousValue}%
</span>` : ''
}
</div>
<input type="range"
min="0"
max="100"
value="${currentValue}"
class="metric-slider"
data-metric="${metric.id}"
oninput="updateSliderValue(this, '${metric.id}', '${metric.color}')"
style="background: linear-gradient(90deg, #e9ecef ${currentValue}%, ${metric.color} ${currentValue}%)">
<div class="slider-value" id="value-${metric.id}">${currentValue}%</div>
<div class="metric-badge ${getBadgeClass(currentValue)}" id="badge-${metric.id}">
${getBadgeText(currentValue)}
</div>
`;
metricsGrid.appendChild(metricCard);
});
// Calculer et afficher l'indice global
updateGlobalIndex();
// Afficher l'évaluation précédente si elle existe
if (previousCalibration) {
document.getElementById('previousEvaluation').style.display = 'block';
const previousMetricsDiv = document.getElementById('previousMetrics');
previousMetricsDiv.innerHTML = `
<p><strong>Date:</strong> ${previousCalibration.date || 'Non spécifiée'}</p>
<p><strong>Expert:</strong> ${previousCalibration.expert || 'Anonyme'}</p>
<p><strong>Indice Global:</strong> ${previousCalibration.moyenne || calculateAverage(normalizedId, previousCalibration)}%</p>
<div class="metric-details">
${metrics.map(m => `
<div style="margin: 5px 0;">
<span>${m.name}:</span>
<span style="float: right; font-weight: bold;">${previousCalibration[m.id] || 0}%</span>
<div class="metric-bar">
<div class="metric-fill" style="width: ${previousCalibration[m.id] || 0}%; background: ${m.color};"></div>
</div>
</div>
`).join('')}
</div>
`;
} else {
document.getElementById('previousEvaluation').style.display = 'none';
}
}
function updateSliderValue(slider, metricId, color) {
const value = slider.value;
document.getElementById(`value-${metricId}`).textContent = `${value}%`;
const badge = document.getElementById(`badge-${metricId}`);
badge.className = `metric-badge ${getBadgeClass(value)}`;
badge.textContent = getBadgeText(value);
// Mettre à jour le fond du slider
slider.style.background = `linear-gradient(90deg, #e9ecef ${value}%, ${color} ${value}%)`;
// Mettre à jour l'indice global
updateGlobalIndex();
}
function updateGlobalIndex() {
const sliders = document.querySelectorAll('.metric-slider');
let sum = 0;
let validSliders = 0;
sliders.forEach(slider => {
const value = parseInt(slider.value);
if (!isNaN(value)) {
sum += value;
validSliders++;
}
});
const average = validSliders > 0 ? sum / validSliders : 0;
document.getElementById('globalIndexValue').textContent = `${average.toFixed(1)}%`;
document.getElementById('globalIndexBar').style.width = `${average}%`;
const badge = document.getElementById('globalIndexBadge');
badge.className = `metric-badge ${getBadgeClass(average)}`;
badge.textContent = `Indice Global: ${getBadgeText(average)} (${average.toFixed(1)}%)`;
}
function getBadgeClass(value) {
if (value >= 80) return 'badge-high';
if (value >= 60) return 'badge-medium';
return 'badge-low';
}
function getBadgeText(value) {
if (value >= 80) return 'Élevé';
if (value >= 60) return 'Modéré';
return 'Faible';
}
function calculateAverage(scenarioId, data) {
const values = metrics.map(m => data[m.id] || 0);
const sum = values.reduce((a, b) => a + b, 0);
return (sum / values.length).toFixed(1);
}
// ============================================
// SAUVEGARDE DE CALIBRATION DANS calibration-trust.json
// ============================================
async function saveCalibration() {
const scenarioId = document.getElementById('scenarioSelect').value;
if (!scenarioId) {
showNotification('Veuillez sélectionner un scénario', 'error');
return;
}
// Normaliser l'ID du scénario
const normalizedId = scenarioId;
// Demander le nom de l'expert
const expertName = prompt('Votre nom (obligatoire pour la sauvegarde):', 'Expert');
if (!expertName || expertName.trim() === '') {
showNotification('Le nom de l\'expert est obligatoire', 'error');
return;
}
// Récupérer les valeurs des sliders
const calibration = {
scenario: normalizedId,
date: new Date().toISOString().split('T')[0],
time: new Date().toTimeString().split(' ')[0],
expert: expertName.trim(),
expert_id: generateExpertId(expertName)
};
// Récupérer les valeurs des métriques
metrics.forEach(metric => {
const slider = document.querySelector(`[data-metric="${metric.id}"]`);
calibration[metric.id] = parseInt(slider.value);
});
// Calculer l'indice global
calibration.moyenne = calculateAverage(normalizedId, calibration);
// Afficher le chargement
document.getElementById('loadingExpert').style.display = 'block';
try {
// 1. Sauvegarder dans localStorage (pour l'interface immédiate)
calibrationData[normalizedId] = calibration;
localStorage.setItem('calibration-trust', JSON.stringify(calibrationData));
// 2. Sauvegarder dans le fichier calibration-trust.json
await saveCalibrationToJSON(calibration);
// 3. Mettre à jour trustStatsData si nécessaire
if (!trustStatsData[normalizedId]) {
trustStatsData[normalizedId] = {};
}
document.getElementById('loadingExpert').style.display = 'none';
showNotification(`✅ Calibration enregistrée pour ${normalizedId}`, 'success');
loadMetricsForScenario(normalizedId);
// 4. Recharger les données si on est sur l'onglet consensus
if (document.getElementById('consensus').classList.contains('active')) {
loadConsensusData();
}
} catch (error) {
document.getElementById('loadingExpert').style.display = 'none';
showNotification(`❌ Erreur: ${error.message}`, 'error');
console.error('Erreur sauvegarde calibration:', error);
}
}
async function saveCalibrationToJSON(newCalibration) {
try {
// 1. Charger le fichier existant
let existingData = { experts: [] };
try {
const response = await fetch('calibration-trust.json');
if (response.ok) {
existingData = await response.json();
}
} catch (error) {
console.log('Fichier calibration-trust.json non trouvé, création d\'un nouveau');
}
// 2. Chercher l'expert existant ou en créer un nouveau
const expertName = newCalibration.expert;
const expertId = newCalibration.expert_id;
let expert = existingData.experts.find(e =>
e.expert_id === expertId || e.expert_name === expertName
);
if (!expert) {
// Créer un nouvel expert
expert = {
expert_id: expertId,
expert_name: expertName,
evaluations: [],
last_updated: new Date().toISOString()
};
existingData.experts.push(expert);
}
// 3. Chercher si une évaluation existe déjà pour ce scénario
const existingEvaluationIndex = expert.evaluations.findIndex(
eval => eval.scenario === newCalibration.scenario
);
// Générer un ID d'évaluation unique
const evaluationId = generateEvaluationId();
if (existingEvaluationIndex !== -1) {
// Mettre à jour l'évaluation existante
expert.evaluations[existingEvaluationIndex] = {
evaluation_id: evaluationId,
...newCalibration
};
} else {
// Ajouter une nouvelle évaluation
expert.evaluations.push({
evaluation_id: evaluationId,
...newCalibration
});
}
// 4. Mettre à jour la date de dernière modification
expert.last_updated = new Date().toISOString();
// 5. Sauvegarder le fichier
await saveJSONToFile('calibration-trust.json', existingData);
console.log('Calibration sauvegardée dans calibration-trust.json:', newCalibration);
} catch (error) {
console.error('Erreur sauvegarde dans JSON:', error);
throw new Error('Impossible de sauvegarder dans le fichier JSON');
}
}
function generateExpertId(expertName) {
// Générer un ID d'expert basé sur le nom et la date
const cleanName = expertName.toLowerCase()
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_]/g, '');
return `expert_${cleanName}_${Date.now()}`;
}
function generateEvaluationId() {
return `eval_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async function saveJSONToFile(filename, data) {
// Dans un environnement réel, cela enverrait les données au serveur
// Pour le moment, nous allons simuler la sauvegarde et offrir un téléchargement
try {
// Tentative d'envoi au serveur (si une API existe)
const response = await fetch('/api/save_calibration', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ filename: filename, data: data })
});
if (response.ok) {
const result = await response.json();
console.log('Sauvegarde réussie via API:', result);
return;
}
} catch (error) {
console.log('API non disponible, sauvegarde locale');
}
// Fallback: Offrir le téléchargement du fichier
const dataStr = JSON.stringify(data, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', filename);
linkElement.style.display = 'none';
document.body.appendChild(linkElement);
linkElement.click();
document.body.removeChild(linkElement);
// Afficher un message d'information
showNotification(
'📥 Fichier calibration-trust.json téléchargé. ' +
'Pour une sauvegarde automatique, ajoutez un endpoint /api/save_calibration à votre backend.',
'success'
);
}
// ============================================
// PARTIE 2 : DASHBOARD CONSENSUS
// ============================================
async function loadConsensusData() {
document.getElementById('loadingConsensus').style.display = 'block';
// Charger les données réelles des fichiers JSON
await loadRealDataFromJSON();
// GÉNÉRER DES DONNÉES DE DÉMONSTRATION SI AUCUNE CALIBRATION N'EXISTE
if (Object.keys(calibrationData).length === 0) {
console.log('Aucune donnée de calibration, génération de données de démonstration...');
calibrationData = generateDemoDataFromRealStats();
}
// S'assurer que scenariosList est chargé
if (scenariosList.length === 0) {
await loadScenariosFromJSON();
}
const calibratedScenarios = Object.keys(calibrationData);
const totalScenarios = scenariosList.length;
// Calculer les statistiques globales BASÉES SUR LES DONNÉES RÉELLES
let totalImprovement = 0;
let scenariosAbove80 = 0;
let maxImprovement = 0;
let maxScenario = '';
let scenariosWithData = 0;
calibratedScenarios.forEach(scenarioId => {
const before = trustStatsData[scenarioId] ? trustStatsData[scenarioId].moyenne : 0;
const after = calibrationData[scenarioId].moyenne;
// Vérifier que les données existent
if (before !== undefined && after !== undefined) {
const improvement = after - before;
totalImprovement += improvement;
scenariosWithData++;
if (after >= 80) scenariosAbove80++;
if (improvement > maxImprovement) {
maxImprovement = improvement;
maxScenario = scenarioId;
}
}
});
const avgImprovement = scenariosWithData > 0
? (totalImprovement / scenariosWithData).toFixed(1)
: 0;
// Mettre à jour les statistiques
document.getElementById('statsOverview').innerHTML = `
<div class="stat-card">
<div class="stat-label">Scénarios calibrés</div>
<div class="stat-value">${scenariosWithData}/${totalScenarios}</div>
<div class="stat-label">(${Math.round(scenariosWithData/totalScenarios*100)}%)</div>
</div>
<div class="stat-card">
<div class="stat-label">Indice Global Moyen</div>
<div class="stat-value">${avgImprovement > 0 ? '+' : ''}${avgImprovement}%</div>
<div class="stat-label">d'amélioration</div>
</div>
<div class="stat-card highlight">
<div class="stat-label">Scénarios >80%</div>
<div class="stat-value">${scenariosAbove80}/${scenariosWithData || 0}</div>
<div class="stat-label">après calibration</div>
</div>
<div class="stat-card">
<div class="stat-label">Amélioration max</div>
<div class="stat-value">+${maxImprovement.toFixed(1)}%</div>
<div class="stat-label">${maxScenario || '-'}</div>
</div>
`;
// Générer les graphiques
generateConsensusCharts();
// Générer le détail par métrique
generateDetailedComparison();
document.getElementById('loadingConsensus').style.display = 'none';
}
function generateDemoDataFromRealStats() {
// Générer des données de démonstration BASÉES SUR LES DONNÉES RÉELLES
const demoData = {};
// Prendre les scénarios qui ont des données dans trustStatsData
const scenariosWithData = Object.keys(trustStatsData).filter(id =>
trustStatsData[id] && trustStatsData[id].moyenne !== undefined
);
// Si pas assez de données, prendre les 8 premiers scénarios
const demoScenarios = scenariosWithData.length > 0
? scenariosWithData.slice(0, 8)
: scenariosList.slice(0, 8).map(s => s.id);
demoScenarios.forEach(scenarioId => {
const baseData = trustStatsData[scenarioId];
if (baseData) {
// Amélioration réaliste basée sur les données existantes
const improvementFactor = 0.05 + (Math.random() * 0.2);
const demoMetrics = {};
metrics.forEach(metric => {
const baseValue = baseData[metric.id] || 50;
demoMetrics[metric.id] = Math.min(100, Math.round(baseValue * (1 + improvementFactor)));
});
const avg = metrics.reduce((sum, metric) => sum + (demoMetrics[metric.id] || 0), 0) / metrics.length;
demoData[scenarioId] = {
scenario: scenarioId,
date: new Date().toISOString().split('T')[0],
expert: "Expert Démo",
expert_id: "expert_demo_1",
...demoMetrics,
moyenne: avg.toFixed(1)
};
}
});
console.log('Données de démonstration générées à partir des stats réelles:', demoData);
return demoData;
}
function generateConsensusCharts() {
const calibratedScenarios = Object.keys(calibrationData);
// Détruire les anciens graphiques s'ils existent
if (mainChart) mainChart.destroy();
if (improvementChart) improvementChart.destroy();
if (detailedChart) detailedChart.destroy();
if (calibratedScenarios.length === 0) {
// Afficher un message dans chaque conteneur de graphique
const noDataHTML = `
<div class="no-data-message">
<h3>📊 Aucune donnée disponible</h3>
<p>Effectuez des calibrations dans l'onglet "Évaluation Expert" pour voir les graphiques ici.</p>
<button onclick="switchTab('expert')">
Aller à l'évaluation Expert
</button>
</div>
`;
document.getElementById('mainTrustChartContainer').innerHTML = noDataHTML;
document.getElementById('improvementChartContainer').innerHTML = noDataHTML;
document.getElementById('detailedMetricsChartContainer').innerHTML = noDataHTML;
return;
}
const labels = calibratedScenarios;
const beforeData = calibratedScenarios.map(id => trustStatsData[id] ? trustStatsData[id].moyenne : 0);
const afterData = calibratedScenarios.map(id => calibrationData[id].moyenne);
const improvementData = calibratedScenarios.map((id, i) => afterData[i] - beforeData[i]);
const targetLine = Array(calibratedScenarios.length).fill(80);
// Graphique 1: Indice Global Avant/Après
const trustCtx = document.getElementById('mainTrustChart').getContext('2d');
mainChart = new Chart(trustCtx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Avant Calibration',
data: beforeData,
backgroundColor: 'rgba(231, 76, 60, 0.8)',
borderColor: 'rgba(231, 76, 60, 1)',
borderWidth: 1
},
{
label: 'Après Calibration',
data: afterData,
backgroundColor: 'rgba(46, 204, 113, 0.8)',
borderColor: 'rgba(46, 204, 113, 1)',
borderWidth: 1
},
{
label: 'Seuil Cible (80%)',
data: targetLine,
type: 'line',
fill: false,
borderColor: 'rgba(241, 196, 15, 0.9)',
borderWidth: 3,
borderDash: [5, 5],
pointRadius: 0
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Indice Global de Confiance (%)'
}
}
}
}
});
// Graphique 2: Amélioration par scénario
const improvementCtx = document.getElementById('improvementChart').getContext('2d');
improvementChart = new Chart(improvementCtx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Amélioration (%)',
data: improvementData,
backgroundColor: improvementData.map(value =>
value > 30 ? 'rgba(46, 204, 113, 0.8)' :
value > 15 ? 'rgba(241, 196, 15, 0.8)' :
'rgba(231, 76, 60, 0.8)'
),
borderColor: improvementData.map(value =>
value > 30 ? 'rgba(46, 204, 113, 1)' :
value > 15 ? 'rgba(241, 196, 15, 1)' :
'rgba(231, 76, 60, 1)'
),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Amélioration (points %)'
}
}
}
}
});
// Graphique 3: Détail des 5 métriques (moyennes)
const detailedCtx = document.getElementById('detailedMetricsChart').getContext('2d');
const metricBeforeData = metrics.map(metric => {
const values = calibratedScenarios.map(id =>
trustStatsData[id] ? trustStatsData[id][metric.id] || 0 : 0
);
return values.reduce((a, b) => a + b, 0) / values.length;
});
const metricAfterData = metrics.map(metric => {
const values = calibratedScenarios.map(id =>
calibrationData[id] ? calibrationData[id][metric.id] || 0 : 0
);
return values.reduce((a, b) => a + b, 0) / values.length;
});
detailedChart = new Chart(detailedCtx, {
type: 'bar',
data: {
labels: metrics.map(m => m.id),
datasets: [
{
label: 'Avant Calibration',
data: metricBeforeData,
backgroundColor: metrics.map(m => m.color + '80'),
borderColor: metrics.map(m => m.color),
borderWidth: 1
},
{
label: 'Après Calibration',
data: metricAfterData,
backgroundColor: metrics.map(m => m.color),
borderColor: metrics.map(m => m.color),
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Score Moyen (%)'
}
}
}
}
});
}
function generateDetailedComparison() {
const calibratedScenarios = Object.keys(calibrationData);
console.log('Scénarios calibrés pour la comparaison détaillée:', calibratedScenarios);
let html = '';
if (calibratedScenarios.length === 0) {
html = `
<div class="no-data-message">
<h3>📋 Aucune donnée disponible</h3>
<p>Effectuez des calibrations dans l'onglet "Évaluation Expert" pour voir les comparaisons détaillées.</p>
<button onclick="switchTab('expert')">
Aller à l'évaluation Expert
</button>
</div>
`;
} else {
// Trier les scénarios par ID
calibratedScenarios.sort((a, b) => {
const numA = parseInt(a.replace('S', ''));
const numB = parseInt(b.replace('S', ''));
return numA - numB;
});
calibratedScenarios.forEach(scenarioId => {
const scenario = scenariosList.find(s => s.id === scenarioId);
const before = trustStatsData[scenarioId] || {};
const after = calibrationData[scenarioId] || {};
console.log(`Génération pour ${scenarioId}:`, {before, after});
html += `
<div style="background: white; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 3px 10px rgba(0,0,0,0.05);">
<h4 style="color: #2c3e50; margin-bottom: 15px;">${scenarioId}: ${scenario ? scenario.name : 'Inconnu'}</h4>
<p style="color: #666; margin-bottom: 15px; font-style: italic;">${scenario ? scenario.description : 'Pas de description'}</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
`;
metrics.forEach(metric => {
// Récupérer les valeurs avec des valeurs par défaut
const beforeValue = before[metric.id] || 0;
const afterValue = after[metric.id] || 0;
const improvement = afterValue - beforeValue;
// Assurer que les valeurs sont des nombres
const safeBeforeValue = Number(beforeValue) || 0;
const safeAfterValue = Number(afterValue) || 0;
const safeImprovement = safeAfterValue - safeBeforeValue;
// Calculer les largeurs pour les barres
const beforeWidth = Math.min(100, Math.max(0, safeBeforeValue));
const improvementWidth = Math.min(100 - beforeWidth, Math.max(0, safeImprovement));
html += `
<div style="padding: 15px; border-radius: 8px; border-left: 4px solid ${metric.color}; background: #f8f9fa;">
<div style="font-weight: bold; margin-bottom: 5px; color: ${metric.color};">${metric.id} - ${metric.name.split('(')[0].trim()}</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="color: #e74c3c; font-size: 14px;">${safeBeforeValue.toFixed(0)}%</span>
<span style="color: #27ae60; font-size: 14px;">${safeAfterValue.toFixed(0)}%</span>
</div>
<div style="height: 6px; background: #e9ecef; border-radius: 3px; margin-bottom: 5px; overflow: hidden;">
<div style="height: 100%; width: ${beforeWidth}%; background: ${metric.color}; opacity: 0.5; float: left;"></div>
<div style="height: 100%; width: ${improvementWidth}%; background: ${metric.color}; float: left;"></div>
</div>
<div style="text-align: right; font-size: 12px; color: ${safeImprovement > 0 ? '#27ae60' : '#e74c3c'}; font-weight: bold;">
${safeImprovement > 0 ? '+' : ''}${safeImprovement.toFixed(1)}%
</div>
</div>
`;
});
// Calculer les moyennes
const beforeAvg = before.moyenne || 0;
const afterAvg = after.moyenne || 0;
const avgImprovement = afterAvg - beforeAvg;
html += `
</div>
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>Indice Global:</strong>
<span style="color: #e74c3c; margin-left: 10px; font-weight: bold;">${Number(beforeAvg).toFixed(1)}%</span>
<span style="margin: 0 10px; font-weight: bold;">→</span>
<span style="color: #27ae60; font-weight: bold;">${Number(afterAvg).toFixed(1)}%</span>
</div>
<div style="padding: 8px 15px; border-radius: 20px; background: ${avgImprovement > 0 ? '#d4edda' : '#f8d7da'}; color: ${avgImprovement > 0 ? '#155724' : '#721c24'}; font-weight: bold;">
${avgImprovement > 0 ? '+' : ''}${Number(avgImprovement).toFixed(1)}%
</div>
</div>
<div style="margin-top: 10px; font-size: 12px; color: #666;">
<strong>Expert:</strong> ${after.expert || 'Non spécifié'} |
<strong>Date:</strong> ${after.date || 'Non spécifiée'}
</div>
</div>
</div>
`;
});
}
document.getElementById('detailedComparison').innerHTML = html;
console.log('HTML généré pour la comparaison détaillée');
}
// ============================================
// FONCTIONS UTILITAIRES
// ============================================
function showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// ============================================
// FONCTIONS D'EXPORT/IMPORT
// ============================================
function exportCalibrationData() {
const dataStr = JSON.stringify(calibrationData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', 'calibration-data-export.json');
linkElement.click();
showNotification('Données exportées en JSON', 'success');
}
function importCalibrationData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = function(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
try {
const importedData = JSON.parse(e.target.result);
calibrationData = { ...calibrationData, ...importedData };
localStorage.setItem('calibration-trust', JSON.stringify(calibrationData));
showNotification('Données importées avec succès', 'success');
if (document.getElementById('consensus').classList.contains('active')) {
loadConsensusData();
}
} catch (error) {
showNotification('Erreur lors de l\'import: ' + error.message, 'error');
}
};
reader.readAsText(file);
};
input.click();
}
function showCalibrationStructure() {
const exampleStructure = {
"experts": [
{
"expert_id": "expert_jean_dupont_1234567890",
"expert_name": "Jean Dupont",
"evaluations": [
{
"evaluation_id": "eval_1234567890_abc123",
"scenario": "S1",
"date": "2024-01-16",
"time": "14:30:00",
"expert": "Jean Dupont",
"expert_id": "expert_jean_dupont_1234567890",
"AR": 85,
"AE": 82,
"ESR": 78,
"SDM": 80,
"SM": 76,
"moyenne": 80.2
}
],
"last_updated": "2024-01-16T14:30:00.000Z"
}
]
};
alert(`Structure de calibration-trust.json :
Le fichier doit contenir un tableau "experts" avec :
1. expert_id : Identifiant unique de l'expert
2. expert_name : Nom de l'expert
3. evaluations : Tableau des évaluations
4. last_updated : Date de dernière modification
Chaque évaluation contient :
- evaluation_id : ID unique
- scenario : ID du scénario (ex: "S1")
- date, time : Date et heure
- expert : Nom de l'expert
- AR, AE, ESR, SDM, SM : Scores 0-100%
- moyenne : Moyenne des 5 métriques
Voir la console pour un exemple complet.`);
console.log('Exemple de structure calibration-trust.json:', exampleStructure);
}
// ============================================
// AJOUT DES BOUTONS D'UTILITAIRE
// ============================================
function addUtilityButtons() {
const expertHeader = document.querySelector('#expert header');
const consensusHeader = document.querySelector('#consensus header');
const buttonsHTML = `
<div style="margin-top: 20px; display: flex; gap: 10px; flex-wrap: wrap;">
<button onclick="showCalibrationStructure()" style="padding: 10px 20px; background: #9b59b6; color: white; border: none; border-radius: 5px; cursor: pointer;">
📋 Voir la structure JSON
</button>
<button onclick="exportCalibrationData()" style="padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer;">
📥 Exporter les données
</button>
<button onclick="importCalibrationData()" style="padding: 10px 20px; background: #2ecc71; color: white; border: none; border-radius: 5px; cursor: pointer;">
📤 Importer des données
</button>
<button onclick="location.reload()" style="padding: 10px 20px; background: #e74c3c; color: white; border: none; border-radius: 5px; cursor: pointer;">
🔄 Rafraîchir
</button>
</div>
`;
if (expertHeader) {
expertHeader.insertAdjacentHTML('beforeend', buttonsHTML);
}
if (consensusHeader) {
consensusHeader.insertAdjacentHTML('beforeend', buttonsHTML);
}
}
// ============================================
// INITIALISATION DE L'APPLICATION
// ============================================
document.addEventListener('DOMContentLoaded', async function() {
// Initialiser les onglets
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', function() {
switchTab(this.getAttribute('data-tab'));
});
});
// Charger les données réelles AVANT de charger les scénarios
await loadRealDataFromJSON();
// Charger les scénarios depuis JSON
await loadScenarios();
// Ajouter les boutons d'utilitaire
setTimeout(addUtilityButtons, 1000);
// Si on est sur l'onglet consensus, charger les données
if (document.getElementById('consensus').classList.contains('active')) {
loadConsensusData();
}
});
</script>
</body>
</html>