| <!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;
|
| }
|
|
|
|
|
| .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); }
|
| }
|
|
|
|
|
| .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);
|
| }
|
|
|
|
|
| .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; }
|
| }
|
|
|
|
|
| .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;
|
| }
|
| }
|
|
|
|
|
| .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>
|
|
|
|
|
| <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">
|
|
|
| </div>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <div class="stats-overview" id="statsOverview">
|
|
|
| </div>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <div class="chart-container">
|
| <h3>📈 Amélioration par Scénario</h3>
|
| <div class="chart-wrapper" id="improvementChartContainer">
|
| <canvas id="improvementChart"></canvas>
|
| </div>
|
| </div>
|
|
|
|
|
| <h2 class="section-title">📋 Détail par Métrique et Scénario</h2>
|
| <div id="detailedComparison">
|
|
|
| </div>
|
|
|
|
|
| <div id="loadingConsensus" class="loading">
|
| <div class="loading-spinner"></div>
|
| <p>Chargement des données de comparaison...</p>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <script>
|
|
|
|
|
|
|
|
|
|
|
| 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'
|
| }
|
| ];
|
|
|
|
|
| let scenariosList = [];
|
|
|
|
|
| let trustStatsData = {};
|
|
|
|
|
| let calibrationData = JSON.parse(localStorage.getItem('calibration-trust')) || {};
|
|
|
|
|
| let mainChart = null;
|
| let improvementChart = null;
|
| let detailedChart = null;
|
|
|
|
|
|
|
|
|
|
|
| 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();
|
|
|
|
|
| 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');
|
| }
|
|
|
|
|
| scenariosList.forEach((scenario, index) => {
|
| if (!scenario.id) {
|
| scenario.id = `S${index + 1}`;
|
| }
|
|
|
| if (typeof scenario.id === 'number') {
|
| scenario.id = `S${scenario.id}`;
|
| }
|
| });
|
|
|
| } catch (error) {
|
| console.error('Erreur de chargement de scenarios_list.json:', error);
|
|
|
| scenariosList = generateDefaultScenarios();
|
| console.log(`Utilisation de ${scenariosList.length} scénarios par défaut`);
|
| }
|
| }
|
|
|
| function generateDefaultScenarios() {
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| async function loadRealDataFromJSON() {
|
| try {
|
|
|
| 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);
|
|
|
|
|
| if (trustData.evaluations && Array.isArray(trustData.evaluations)) {
|
| trustStatsData = {};
|
|
|
| trustData.evaluations.forEach(eval => {
|
| const scenarioId = `S${eval.scenario_id}`;
|
|
|
|
|
| const metricsData = eval.trust_metrics || {};
|
| const normalizedMetrics = {
|
| AR: (metricsData.RA || metricsData.AR || 0) * 20,
|
| 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
|
| };
|
|
|
|
|
| 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);
|
| }
|
| }
|
|
|
|
|
| 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);
|
|
|
|
|
| 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) {
|
|
|
| calibrationData[evaluation.scenario] = evaluation;
|
| }
|
| });
|
| }
|
| });
|
| }
|
|
|
| console.log('calibrationData chargé depuis JSON:', calibrationData);
|
|
|
|
|
| localStorage.setItem('calibration-trust', JSON.stringify(calibrationData));
|
| }
|
|
|
|
|
| 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);
|
|
|
| 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
|
| };
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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();
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
| async function loadScenarios() {
|
| const select = document.getElementById('scenarioSelect');
|
| select.innerHTML = '<option value="">Chargement des scénarios...</option>';
|
|
|
|
|
| await loadScenariosFromJSON();
|
|
|
|
|
| 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);
|
| });
|
|
|
|
|
| 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é';
|
| }
|
| });
|
|
|
|
|
| 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 = '';
|
|
|
|
|
| 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);
|
| });
|
|
|
|
|
| updateGlobalIndex();
|
|
|
|
|
| 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);
|
|
|
|
|
| slider.style.background = `linear-gradient(90deg, #e9ecef ${value}%, ${color} ${value}%)`;
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
|
|
|
|
|
|
| async function saveCalibration() {
|
| const scenarioId = document.getElementById('scenarioSelect').value;
|
|
|
| if (!scenarioId) {
|
| showNotification('Veuillez sélectionner un scénario', 'error');
|
| return;
|
| }
|
|
|
|
|
| const normalizedId = scenarioId;
|
|
|
|
|
| const expertName = prompt('Votre nom (obligatoire pour la sauvegarde):', 'Expert');
|
| if (!expertName || expertName.trim() === '') {
|
| showNotification('Le nom de l\'expert est obligatoire', 'error');
|
| return;
|
| }
|
|
|
|
|
| const calibration = {
|
| scenario: normalizedId,
|
| date: new Date().toISOString().split('T')[0],
|
| time: new Date().toTimeString().split(' ')[0],
|
| expert: expertName.trim(),
|
| expert_id: generateExpertId(expertName)
|
| };
|
|
|
|
|
| metrics.forEach(metric => {
|
| const slider = document.querySelector(`[data-metric="${metric.id}"]`);
|
| calibration[metric.id] = parseInt(slider.value);
|
| });
|
|
|
|
|
| calibration.moyenne = calculateAverage(normalizedId, calibration);
|
|
|
|
|
| document.getElementById('loadingExpert').style.display = 'block';
|
|
|
| try {
|
|
|
| calibrationData[normalizedId] = calibration;
|
| localStorage.setItem('calibration-trust', JSON.stringify(calibrationData));
|
|
|
|
|
| await saveCalibrationToJSON(calibration);
|
|
|
|
|
| if (!trustStatsData[normalizedId]) {
|
| trustStatsData[normalizedId] = {};
|
| }
|
|
|
| document.getElementById('loadingExpert').style.display = 'none';
|
| showNotification(`✅ Calibration enregistrée pour ${normalizedId}`, 'success');
|
| loadMetricsForScenario(normalizedId);
|
|
|
|
|
| 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 {
|
|
|
| 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');
|
| }
|
|
|
|
|
| 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) {
|
|
|
| expert = {
|
| expert_id: expertId,
|
| expert_name: expertName,
|
| evaluations: [],
|
| last_updated: new Date().toISOString()
|
| };
|
| existingData.experts.push(expert);
|
| }
|
|
|
|
|
| const existingEvaluationIndex = expert.evaluations.findIndex(
|
| eval => eval.scenario === newCalibration.scenario
|
| );
|
|
|
|
|
| const evaluationId = generateEvaluationId();
|
|
|
| if (existingEvaluationIndex !== -1) {
|
|
|
| expert.evaluations[existingEvaluationIndex] = {
|
| evaluation_id: evaluationId,
|
| ...newCalibration
|
| };
|
| } else {
|
|
|
| expert.evaluations.push({
|
| evaluation_id: evaluationId,
|
| ...newCalibration
|
| });
|
| }
|
|
|
|
|
| expert.last_updated = new Date().toISOString();
|
|
|
|
|
| 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) {
|
|
|
| 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) {
|
|
|
|
|
|
|
| try {
|
|
|
| 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');
|
| }
|
|
|
|
|
| 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);
|
|
|
|
|
| showNotification(
|
| '📥 Fichier calibration-trust.json téléchargé. ' +
|
| 'Pour une sauvegarde automatique, ajoutez un endpoint /api/save_calibration à votre backend.',
|
| 'success'
|
| );
|
| }
|
|
|
|
|
|
|
|
|
|
|
| async function loadConsensusData() {
|
| document.getElementById('loadingConsensus').style.display = 'block';
|
|
|
|
|
| await loadRealDataFromJSON();
|
|
|
|
|
| if (Object.keys(calibrationData).length === 0) {
|
| console.log('Aucune donnée de calibration, génération de données de démonstration...');
|
| calibrationData = generateDemoDataFromRealStats();
|
| }
|
|
|
|
|
| if (scenariosList.length === 0) {
|
| await loadScenariosFromJSON();
|
| }
|
|
|
| const calibratedScenarios = Object.keys(calibrationData);
|
| const totalScenarios = scenariosList.length;
|
|
|
|
|
| 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;
|
|
|
|
|
| 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;
|
|
|
|
|
| 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>
|
| `;
|
|
|
|
|
| generateConsensusCharts();
|
|
|
|
|
| generateDetailedComparison();
|
|
|
| document.getElementById('loadingConsensus').style.display = 'none';
|
| }
|
|
|
| function generateDemoDataFromRealStats() {
|
|
|
| const demoData = {};
|
|
|
|
|
| const scenariosWithData = Object.keys(trustStatsData).filter(id =>
|
| trustStatsData[id] && trustStatsData[id].moyenne !== undefined
|
| );
|
|
|
|
|
| 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) {
|
|
|
| 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);
|
|
|
|
|
| if (mainChart) mainChart.destroy();
|
| if (improvementChart) improvementChart.destroy();
|
| if (detailedChart) detailedChart.destroy();
|
|
|
| if (calibratedScenarios.length === 0) {
|
|
|
| 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);
|
|
|
|
|
| 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 (%)'
|
| }
|
| }
|
| }
|
| }
|
| });
|
|
|
|
|
| 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 %)'
|
| }
|
| }
|
| }
|
| }
|
| });
|
|
|
|
|
| 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 {
|
|
|
| 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 => {
|
|
|
| const beforeValue = before[metric.id] || 0;
|
| const afterValue = after[metric.id] || 0;
|
| const improvement = afterValue - beforeValue;
|
|
|
|
|
| const safeBeforeValue = Number(beforeValue) || 0;
|
| const safeAfterValue = Number(afterValue) || 0;
|
| const safeImprovement = safeAfterValue - safeBeforeValue;
|
|
|
|
|
| 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>
|
| `;
|
| });
|
|
|
|
|
| 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');
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', async function() {
|
|
|
| document.querySelectorAll('.tab-button').forEach(button => {
|
| button.addEventListener('click', function() {
|
| switchTab(this.getAttribute('data-tab'));
|
| });
|
| });
|
|
|
|
|
| await loadRealDataFromJSON();
|
|
|
|
|
| await loadScenarios();
|
|
|
|
|
| setTimeout(addUtilityButtons, 1000);
|
|
|
|
|
| if (document.getElementById('consensus').classList.contains('active')) {
|
| loadConsensusData();
|
| }
|
| });
|
| </script>
|
| </body>
|
| </html> |