Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SysCRED - Vérification de Crédibilité</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <script src="/static/js/d3.min.js"></script> | |
| <style> | |
| .graph-container { | |
| width: 100%; | |
| height: 500px; | |
| min-height: 500px; | |
| background: rgba(0, 0, 0, 0.2); | |
| border-radius: 12px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| position: relative; | |
| display: block; | |
| /* Force display */ | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0d0d1f 100%); | |
| min-height: 100vh; | |
| color: #e0e0e0; | |
| padding: 2rem; | |
| position: relative; | |
| } | |
| /* Permanent Blue Glow Border Animation - Subtle border only */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| pointer-events: none; | |
| border: 3px solid rgba(0, 150, 255, 0.6); | |
| border-radius: 0; | |
| background: transparent; | |
| animation: blueGlowPulse 3s ease-in-out infinite; | |
| z-index: 9999; | |
| } | |
| @keyframes blueGlowPulse { | |
| 0%, 100% { | |
| border-color: rgba(0, 150, 255, 0.5); | |
| box-shadow: inset 0 0 15px rgba(0, 150, 255, 0.15), | |
| 0 0 20px rgba(0, 150, 255, 0.3); | |
| } | |
| 50% { | |
| border-color: rgba(100, 200, 255, 0.8); | |
| box-shadow: inset 0 0 25px rgba(0, 180, 255, 0.2), | |
| 0 0 35px rgba(0, 180, 255, 0.5); | |
| } | |
| } | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 3rem; | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #00d4ff, #7c3aed, #f472b6); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 0.5rem; | |
| } | |
| .subtitle { | |
| color: #8b8ba7; | |
| font-size: 1.1rem; | |
| } | |
| .search-box { | |
| background: rgba(255, 255, 255, 0.03); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| border-radius: 20px; | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 1rem; | |
| align-items: center; | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| padding: 1rem 1.5rem; | |
| font-size: 1rem; | |
| border: 2px solid rgba(124, 58, 237, 0.3); | |
| border-radius: 12px; | |
| background: rgba(0, 0, 0, 0.3); | |
| color: #fff; | |
| transition: all 0.3s ease; | |
| } | |
| input[type="text"]:focus { | |
| outline: none; | |
| border-color: #7c3aed; | |
| box-shadow: 0 0 20px rgba(124, 58, 237, 0.3); | |
| } | |
| input[type="text"]::placeholder { | |
| color: #6b6b8a; | |
| } | |
| button { | |
| padding: 1rem 2rem; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| border: none; | |
| border-radius: 12px; | |
| background: linear-gradient(135deg, #7c3aed, #a855f7); | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| white-space: nowrap; | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 30px rgba(124, 58, 237, 0.4); | |
| } | |
| button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .results { | |
| display: none; | |
| } | |
| .results.visible { | |
| display: block; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* ======================================== | |
| NER ENTITIES STYLES | |
| ======================================== */ | |
| .ner-section { | |
| background: rgba(0, 212, 255, 0.05); | |
| border: 1px solid rgba(0, 212, 255, 0.2); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .ner-entities { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.75rem; | |
| } | |
| .ner-tag { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.5rem 1rem; | |
| border-radius: 20px; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| } | |
| .ner-tag:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.3); | |
| } | |
| .ner-tag.person { background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(239, 68, 68, 0.1)); border: 1px solid rgba(239, 68, 68, 0.5); color: #fca5a5; } | |
| .ner-tag.org { background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(59, 130, 246, 0.1)); border: 1px solid rgba(59, 130, 246, 0.5); color: #93c5fd; } | |
| .ner-tag.location { background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(16, 185, 129, 0.1)); border: 1px solid rgba(16, 185, 129, 0.5); color: #6ee7b7; } | |
| .ner-tag.date { background: linear-gradient(135deg, rgba(245, 158, 11, 0.3), rgba(245, 158, 11, 0.1)); border: 1px solid rgba(245, 158, 11, 0.5); color: #fcd34d; } | |
| .ner-tag.misc { background: linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(139, 92, 246, 0.1)); border: 1px solid rgba(139, 92, 246, 0.5); color: #c4b5fd; } | |
| .ner-tag-icon { font-size: 1.1rem; } | |
| .ner-tag-text { font-weight: 600; } | |
| .ner-tag-type { font-size: 0.75rem; opacity: 0.8; } | |
| /* ======================================== | |
| E-E-A-T PROGRESS BARS STYLES | |
| ======================================== */ | |
| .eeat-section { | |
| background: rgba(16, 185, 129, 0.05); | |
| border: 1px solid rgba(16, 185, 129, 0.2); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .eeat-bars { | |
| display: grid; | |
| gap: 1rem; | |
| } | |
| .eeat-bar-item { | |
| background: rgba(255,255,255,0.02); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| } | |
| .eeat-label { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 0.75rem; | |
| font-weight: 500; | |
| } | |
| .eeat-value { | |
| background: rgba(16, 185, 129, 0.2); | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 10px; | |
| font-weight: 700; | |
| font-size: 0.9rem; | |
| color: #10b981; | |
| } | |
| .eeat-bar-container { | |
| height: 12px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 6px; | |
| overflow: hidden; | |
| } | |
| .eeat-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, #10b981, #34d399); | |
| border-radius: 6px; | |
| transition: width 1s ease-out; | |
| } | |
| .eeat-bar.expertise { background: linear-gradient(90deg, #3b82f6, #60a5fa); } | |
| .eeat-bar.authority { background: linear-gradient(90deg, #8b5cf6, #a78bfa); } | |
| .eeat-bar.trust { background: linear-gradient(90deg, #f59e0b, #fbbf24); } | |
| .eeat-overall { | |
| margin-top: 1.5rem; | |
| text-align: center; | |
| padding: 1rem; | |
| background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05)); | |
| border-radius: 12px; | |
| border: 1px solid rgba(16, 185, 129, 0.3); | |
| font-size: 1.1rem; | |
| } | |
| .eeat-overall strong { | |
| color: #10b981; | |
| font-size: 1.5rem; | |
| margin-left: 0.5rem; | |
| } | |
| /* ======================================== | |
| WHY THIS RESULT SECTION | |
| ======================================== */ | |
| .why-section { | |
| background: rgba(245, 158, 11, 0.05); | |
| border: 1px solid rgba(245, 158, 11, 0.2); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| .why-content { | |
| display: grid; | |
| gap: 1rem; | |
| } | |
| .why-item { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 1rem; | |
| padding: 1rem; | |
| background: rgba(255,255,255,0.02); | |
| border-radius: 10px; | |
| border-left: 3px solid #f59e0b; | |
| } | |
| .why-icon { | |
| font-size: 1.5rem; | |
| min-width: 2rem; | |
| } | |
| .why-text { | |
| flex: 1; | |
| } | |
| .why-text-title { | |
| font-weight: 600; | |
| color: #fbbf24; | |
| margin-bottom: 0.25rem; | |
| } | |
| .why-text-desc { | |
| color: #9ca3af; | |
| font-size: 0.9rem; | |
| line-height: 1.5; | |
| } | |
| .score-card { | |
| background: rgba(255, 255, 255, 0.03); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| border-radius: 20px; | |
| padding: 2rem; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .score-value { | |
| font-size: 4rem; | |
| font-weight: 700; | |
| margin: 1rem 0; | |
| } | |
| .score-high { | |
| color: #22c55e; | |
| } | |
| .score-medium { | |
| color: #eab308; | |
| } | |
| .score-low { | |
| color: #ef4444; | |
| } | |
| .score-label { | |
| font-size: 1.2rem; | |
| color: #8b8ba7; | |
| margin-bottom: 1rem; | |
| } | |
| .credibility-badge { | |
| display: inline-block; | |
| padding: 0.5rem 1.5rem; | |
| border-radius: 50px; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .badge-high { | |
| background: rgba(34, 197, 94, 0.2); | |
| color: #22c55e; | |
| border: 1px solid #22c55e; | |
| } | |
| .badge-medium { | |
| background: rgba(234, 179, 8, 0.2); | |
| color: #eab308; | |
| border: 1px solid #eab308; | |
| } | |
| .badge-low { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: #ef4444; | |
| border: 1px solid #ef4444; | |
| } | |
| .details-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| margin-bottom: 2rem; | |
| } | |
| .detail-card { | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| } | |
| .detail-label { | |
| font-size: 0.85rem; | |
| color: #8b8ba7; | |
| margin-bottom: 0.5rem; | |
| } | |
| .detail-value { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: #fff; | |
| } | |
| .summary-box { | |
| background: rgba(124, 58, 237, 0.1); | |
| border: 1px solid rgba(124, 58, 237, 0.3); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .summary-title { | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| color: #a855f7; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 3rem; | |
| display: none; | |
| } | |
| .loading.visible { | |
| display: block; | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 3px solid rgba(124, 58, 237, 0.2); | |
| border-top-color: #7c3aed; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .error { | |
| background: rgba(239, 68, 68, 0.1); | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| color: #ef4444; | |
| display: none; | |
| } | |
| .error.visible { | |
| display: block; | |
| } | |
| footer { | |
| text-align: center; | |
| margin-top: 3rem; | |
| color: #6b6b8a; | |
| font-size: 0.9rem; | |
| } | |
| footer a { | |
| color: #7c3aed; | |
| text-decoration: none; | |
| } | |
| /* Node Details Overlay */ | |
| .node-details-overlay { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(15, 15, 35, 0.95); | |
| border: 1px solid rgba(124, 58, 237, 0.3); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| width: 300px; | |
| display: none; | |
| backdrop-filter: blur(10px); | |
| z-index: 100; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| pointer-events: auto; | |
| } | |
| .node-details-overlay.visible { | |
| display: block; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .close-btn { | |
| position: absolute; | |
| top: 10px; | |
| right: 15px; | |
| background: none; | |
| border: none; | |
| color: #8b8ba7; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| padding: 0; | |
| line-height: 1; | |
| width: auto; | |
| height: auto; | |
| box-shadow: none; | |
| } | |
| .close-btn:hover { | |
| color: #fff; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| /* Backend Toggle Switch */ | |
| .backend-toggle { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.75rem; | |
| margin-top: 1rem; | |
| padding: 0.75rem; | |
| background: rgba(0,0,0,0.2); | |
| border-radius: 10px; | |
| } | |
| .backend-toggle label { | |
| font-size: 0.85rem; | |
| color: #8b8ba7; | |
| cursor: pointer; | |
| } | |
| .backend-toggle .active { | |
| color: #a855f7; | |
| font-weight: 600; | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 50px; | |
| height: 26px; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .toggle-slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background: linear-gradient(135deg, #22c55e, #16a34a); | |
| border-radius: 26px; | |
| transition: 0.3s; | |
| } | |
| .toggle-slider:before { | |
| position: absolute; | |
| content: ''; | |
| height: 20px; | |
| width: 20px; | |
| left: 3px; | |
| bottom: 3px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: 0.3s; | |
| } | |
| .toggle-switch input:checked + .toggle-slider { | |
| background: linear-gradient(135deg, #7c3aed, #a855f7); | |
| } | |
| .toggle-switch input:checked + .toggle-slider:before { | |
| transform: translateX(24px); | |
| } | |
| .backend-status { | |
| font-size: 0.75rem; | |
| color: #6b6b8a; | |
| text-align: center; | |
| margin-top: 0.5rem; | |
| } | |
| .backend-status.local { color: #22c55e; } | |
| .backend-status.remote { color: #a855f7; } | |
| /* === EXPLAINABILITY MODAL === */ | |
| .explainer-modal { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(10px); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| } | |
| .explainer-modal.visible { display: flex; } | |
| .explainer-content { | |
| background: linear-gradient(135deg, #1a1a3e 0%, #0f0f23 100%); | |
| border: 1px solid rgba(124, 58, 237, 0.5); | |
| border-radius: 20px; | |
| max-width: 600px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| padding: 2rem; | |
| animation: slideUp 0.3s ease; | |
| } | |
| @keyframes slideUp { | |
| from { transform: translateY(50px); opacity: 0; } | |
| to { transform: translateY(0); opacity: 1; } | |
| } | |
| .explainer-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1.5rem; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| padding-bottom: 1rem; | |
| } | |
| .explainer-title { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: #fff; | |
| } | |
| .explainer-close { | |
| background: none; | |
| border: none; | |
| color: #8b8ba7; | |
| font-size: 2rem; | |
| cursor: pointer; | |
| padding: 0; | |
| line-height: 1; | |
| } | |
| .explainer-close:hover { color: #fff; transform: none; box-shadow: none; } | |
| .metric-explain { | |
| background: rgba(255,255,255,0.03); | |
| border-radius: 12px; | |
| padding: 1.25rem; | |
| margin-bottom: 1rem; | |
| border-left: 4px solid #7c3aed; | |
| } | |
| .metric-explain-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| .metric-explain-icon { | |
| font-size: 1.5rem; | |
| } | |
| .metric-explain-name { | |
| font-weight: 600; | |
| color: #fff; | |
| font-size: 1.1rem; | |
| } | |
| .metric-explain-value { | |
| background: rgba(124, 58, 237, 0.2); | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 6px; | |
| font-weight: 700; | |
| color: #a855f7; | |
| margin-left: auto; | |
| } | |
| .metric-explain-simple { | |
| color: #e0e0e0; | |
| font-size: 1rem; | |
| line-height: 1.6; | |
| margin-bottom: 0.5rem; | |
| } | |
| .metric-explain-detail { | |
| color: #8b8ba7; | |
| font-size: 0.85rem; | |
| font-style: italic; | |
| } | |
| .score-clickable { | |
| cursor: pointer; | |
| transition: transform 0.2s ease; | |
| } | |
| .score-clickable:hover { | |
| transform: scale(1.05); | |
| } | |
| .help-icon { | |
| display: inline-block; | |
| width: 18px; | |
| height: 18px; | |
| background: rgba(124, 58, 237, 0.3); | |
| border-radius: 50%; | |
| text-align: center; | |
| line-height: 18px; | |
| font-size: 12px; | |
| color: #a855f7; | |
| cursor: pointer; | |
| margin-left: 0.5rem; | |
| vertical-align: middle; | |
| } | |
| .help-icon:hover { | |
| background: rgba(124, 58, 237, 0.6); | |
| color: #fff; | |
| } | |
| .verdict-bar { | |
| height: 8px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| margin: 0.5rem 0; | |
| } | |
| .verdict-fill { | |
| height: 100%; | |
| border-radius: 4px; | |
| transition: width 0.5s ease; | |
| } | |
| .verdict-labels { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.75rem; | |
| color: #6b6b8a; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>🔬 SysCRED</h1> | |
| <p class="subtitle">Système Neuro-Symbolique de Vérification de Crédibilité</p> | |
| </header> | |
| <div class="search-box"> | |
| <div class="input-group"> | |
| <input type="text" id="urlInput" placeholder="Entrez une URL ou du texte à analyser" | |
| autofocus> | |
| <button id="analyzeBtn" onclick="analyzeUrl()"> | |
| 🔍 Analyser | |
| </button> | |
| </div> | |
| <!-- Backend Toggle --> | |
| <div class="backend-toggle"> | |
| <label id="labelLocal" class="active">🖥️ Local</label> | |
| <div class="toggle-switch"> | |
| <input type="checkbox" id="backendToggle" onchange="toggleBackend()"> | |
| <span class="toggle-slider"></span> | |
| </div> | |
| <label id="labelRemote">☁️ HF Space</label> | |
| </div> | |
| <div class="backend-status local" id="backendStatus">Backend: localhost:5001 (léger, sans ML)</div> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Analyse en cours...</p> | |
| </div> | |
| <div class="error" id="error"></div> | |
| <div class="results" id="results"> | |
| <div class="score-card"> | |
| <div class="score-label">Score de Crédibilité <span class="help-icon" onclick="event.stopPropagation(); openExplainer()">?</span></div> | |
| <div class="score-value score-clickable" id="scoreValue" onclick="openExplainer()" title="Cliquez pour comprendre ce score">0.00</div> | |
| <div class="credibility-badge" id="credibilityBadge">-</div> | |
| <small style="color: #6b6b8a; margin-top: 0.5rem; display: block;">👆 Cliquez sur le score pour comprendre</small> | |
| </div> | |
| <div class="summary-box"> | |
| <div class="summary-title">📋 Résumé de l'analyse</div> | |
| <p id="summary">-</p> | |
| </div> | |
| <div class="details-grid" id="detailsGrid"></div> | |
| <!-- NER ENTITIES SECTION --> | |
| <div class="ner-section" id="nerSection" style="display: none;"> | |
| <div class="summary-title" style="margin-bottom: 1rem; color: #00d4ff;">🏷️ Entités Détectées (NER)</div> | |
| <div class="ner-entities" id="nerEntities"> | |
| <!-- Filled dynamically --> | |
| </div> | |
| </div> | |
| <!-- E-E-A-T METRICS SECTION --> | |
| <div class="eeat-section" id="eeatSection" style="display: none;"> | |
| <div class="summary-title" style="margin-bottom: 1rem; color: #10b981;"> | |
| 📊 Score E-E-A-T | |
| <span class="help-icon" onclick="event.stopPropagation(); showEEATExplainer()" title="Experience, Expertise, Authority, Trust - Critères Google">?</span> | |
| </div> | |
| <p style="color: #8b8ba7; font-size: 0.9rem; margin-bottom: 1rem;">Critères inspirés du système de qualité de Google</p> | |
| <div class="eeat-bars"> | |
| <div class="eeat-bar-item"> | |
| <div class="eeat-label"> | |
| <span>🎯 Experience</span> | |
| <span class="eeat-value" id="eeatExperience">--</span> | |
| </div> | |
| <div class="eeat-bar-container"> | |
| <div class="eeat-bar" id="eeatExperienceBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="eeat-bar-item"> | |
| <div class="eeat-label"> | |
| <span>🧠 Expertise</span> | |
| <span class="eeat-value" id="eeatExpertise">--</span> | |
| </div> | |
| <div class="eeat-bar-container"> | |
| <div class="eeat-bar expertise" id="eeatExpertiseBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="eeat-bar-item"> | |
| <div class="eeat-label"> | |
| <span>🏛️ Authority</span> | |
| <span class="eeat-value" id="eeatAuthority">--</span> | |
| </div> | |
| <div class="eeat-bar-container"> | |
| <div class="eeat-bar authority" id="eeatAuthorityBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="eeat-bar-item"> | |
| <div class="eeat-label"> | |
| <span>🛡️ Trust</span> | |
| <span class="eeat-value" id="eeatTrust">--</span> | |
| </div> | |
| <div class="eeat-bar-container"> | |
| <div class="eeat-bar trust" id="eeatTrustBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="eeat-overall" id="eeatOverall"> | |
| Score Global E-E-A-T: <strong>--</strong> | |
| </div> | |
| </div> | |
| <!-- TREC IR METRICS SECTION --> | |
| <div class="trec-section" id="trecSection" style="display: none; background: rgba(139, 92, 246, 0.05); border: 1px solid rgba(139, 92, 246, 0.2); border-radius: 16px; padding: 1.5rem; margin-bottom: 2rem;"> | |
| <div class="summary-title" style="margin-bottom: 1rem; color: #8b5cf6;"> | |
| 📈 Métriques IR (TREC) | |
| <span class="help-icon" onclick="event.stopPropagation(); showTRECExplainer()" title="Information Retrieval metrics from TREC evaluation">?</span> | |
| </div> | |
| <p style="color: #8b8ba7; font-size: 0.9rem; margin-bottom: 1rem;">Métriques d'évaluation de recherche d'information</p> | |
| <div class="trec-metrics-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1rem;"> | |
| <div class="trec-metric-card" style="background: rgba(255,255,255,0.02); border-radius: 10px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #8b8ba7; margin-bottom: 0.5rem;">Precision</div> | |
| <div id="trecPrecision" style="font-size: 1.5rem; font-weight: 700; color: #a78bfa;">--</div> | |
| </div> | |
| <div class="trec-metric-card" style="background: rgba(255,255,255,0.02); border-radius: 10px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #8b8ba7; margin-bottom: 0.5rem;">Recall</div> | |
| <div id="trecRecall" style="font-size: 1.5rem; font-weight: 700; color: #a78bfa;">--</div> | |
| </div> | |
| <div class="trec-metric-card" style="background: rgba(255,255,255,0.02); border-radius: 10px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #8b8ba7; margin-bottom: 0.5rem;">MAP</div> | |
| <div id="trecMAP" style="font-size: 1.5rem; font-weight: 700; color: #a78bfa;">--</div> | |
| </div> | |
| <div class="trec-metric-card" style="background: rgba(255,255,255,0.02); border-radius: 10px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #8b8ba7; margin-bottom: 0.5rem;">NDCG</div> | |
| <div id="trecNDCG" style="font-size: 1.5rem; font-weight: 700; color: #a78bfa;">--</div> | |
| </div> | |
| <div class="trec-metric-card" style="background: rgba(255,255,255,0.02); border-radius: 10px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #8b8ba7; margin-bottom: 0.5rem;">TF-IDF Score</div> | |
| <div id="trecTFIDF" style="font-size: 1.5rem; font-weight: 700; color: #a78bfa;">--</div> | |
| </div> | |
| <div class="trec-metric-card" style="background: rgba(255,255,255,0.02); border-radius: 10px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #8b8ba7; margin-bottom: 0.5rem;">MRR</div> | |
| <div id="trecMRR" style="font-size: 1.5rem; font-weight: 700; color: #a78bfa;">--</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GOOGLE-STYLE EXPLANATION --> | |
| <div class="why-section" id="whySection" style="display: none;"> | |
| <div class="summary-title" style="margin-bottom: 1rem; color: #f59e0b;"> | |
| 🔍 Pourquoi ce résultat ? | |
| </div> | |
| <div class="why-content" id="whyContent"> | |
| <!-- Filled dynamically with Google-style explanations --> | |
| </div> | |
| </div> | |
| <div class="graph-section" style="margin-top: 3rem;"> | |
| <div class="summary-title" style="margin-bottom: 2rem; color: #60a5fa;">🕸️ Réseau Neuro-Symbolique | |
| (Ontologie)</div> | |
| <!-- Debug link --> | |
| <small style="color: #666; cursor: pointer;" | |
| onclick="alert('D3 Loaded: ' + (typeof d3 !== 'undefined'))">Debug: Vérifier D3</small> | |
| <div id="cy" class="graph-container"></div> | |
| </div> | |
| </div> | |
| <!-- EXPLAINER MODAL --> | |
| <div class="explainer-modal" id="explainerModal" onclick="if(event.target === this) closeExplainer()"> | |
| <div class="explainer-content"> | |
| <div class="explainer-header"> | |
| <div class="explainer-title">🔍 Comprendre votre score</div> | |
| <button class="explainer-close" onclick="closeExplainer()">×</button> | |
| </div> | |
| <div id="explainerBody"> | |
| <!-- Content filled dynamically --> | |
| </div> | |
| <div style="margin-top: 1.5rem; text-align: center;"> | |
| <button onclick="closeExplainer()" style="background: linear-gradient(135deg, #7c3aed, #a855f7); padding: 0.75rem 2rem; border-radius: 10px; border: none; color: white; font-weight: 600; cursor: pointer;">✓ Fermer</button> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| <p>SysCRED v2.0 - Prototype de recherche doctorale</p> | |
| <p>© Dominique S. Loyer - UQAM | <a href="https://doi.org/10.5281/zenodo.17943226" target="_blank">DOI: | |
| 10.5281/zenodo.17943226</a></p> | |
| </footer> | |
| </div> | |
| <script> | |
| // Backend URLs | |
| // Empty string means relative path (same domain), which works for HF Space | |
| const REMOTE_API_URL = ''; | |
| const LOCAL_API_URL = 'http://localhost:5001'; | |
| // Detect if we are already on localhost | |
| const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; | |
| let API_URL = isLocalhost ? LOCAL_API_URL : REMOTE_API_URL; | |
| // Set initial toggle state based on environment | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const toggle = document.getElementById('backendToggle'); | |
| const status = document.getElementById('backendStatus'); | |
| const labelLocal = document.getElementById('labelLocal'); | |
| const labelRemote = document.getElementById('labelRemote'); | |
| if (isLocalhost) { | |
| toggle.checked = false; | |
| status.textContent = 'Backend: localhost:5001 (léger, sans ML)'; | |
| status.className = 'backend-status local'; | |
| labelLocal.classList.add('active'); | |
| labelRemote.classList.remove('active'); | |
| } else { | |
| toggle.checked = true; | |
| status.textContent = 'Backend: HF Space (ML complet)'; | |
| status.className = 'backend-status remote'; | |
| labelLocal.classList.remove('active'); | |
| labelRemote.classList.add('active'); | |
| } | |
| }); | |
| function toggleBackend() { | |
| const toggle = document.getElementById('backendToggle'); | |
| const status = document.getElementById('backendStatus'); | |
| const labelLocal = document.getElementById('labelLocal'); | |
| const labelRemote = document.getElementById('labelRemote'); | |
| if (toggle.checked) { | |
| API_URL = REMOTE_API_URL; | |
| status.textContent = 'Backend: HF Space (ML complet)'; | |
| status.className = 'backend-status remote'; | |
| labelLocal.classList.remove('active'); | |
| labelRemote.classList.add('active'); | |
| } else { | |
| API_URL = LOCAL_API_URL; | |
| status.textContent = 'Backend: localhost:5001 (léger, sans ML)'; | |
| status.className = 'backend-status local'; | |
| labelLocal.classList.add('active'); | |
| labelRemote.classList.remove('active'); | |
| } | |
| console.log('[SysCRED] Backend switched to:', API_URL || 'Relative Path (HF Space)'); | |
| } | |
| async function analyzeUrl() { | |
| const urlInput = document.getElementById('urlInput'); | |
| const loading = document.getElementById('loading'); | |
| const results = document.getElementById('results'); | |
| const error = document.getElementById('error'); | |
| const btn = document.getElementById('analyzeBtn'); | |
| const inputData = urlInput.value.trim(); | |
| if (!inputData) { | |
| alert('Veuillez entrer une URL'); | |
| return; | |
| } | |
| // Reset UI | |
| results.classList.remove('visible'); | |
| error.classList.remove('visible'); | |
| loading.classList.add('visible'); | |
| btn.disabled = true; | |
| try { | |
| const response = await fetch(`${API_URL}/api/verify`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| input_data: inputData, | |
| include_seo: true, | |
| include_pagerank: true | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Erreur lors de l\'analyse'); | |
| } | |
| displayResults(data); | |
| } catch (err) { | |
| error.textContent = `❌ Erreur: ${err.message}`; | |
| error.classList.add('visible'); | |
| } finally { | |
| loading.classList.remove('visible'); | |
| btn.disabled = false; | |
| } | |
| } | |
| function displayResults(data) { | |
| const results = document.getElementById('results'); | |
| const scoreValue = document.getElementById('scoreValue'); | |
| const credibilityBadge = document.getElementById('credibilityBadge'); | |
| const summary = document.getElementById('summary'); | |
| const detailsGrid = document.getElementById('detailsGrid'); | |
| // Score | |
| const score = data.scoreCredibilite || 0; | |
| scoreValue.textContent = score.toFixed(2); | |
| // Conditional Display: Hide Score Card if TEXT input, show if URL | |
| const isUrl = data.informationEntree && data.informationEntree.startsWith('http'); | |
| const scoreCard = document.querySelector('.score-card'); | |
| if (isUrl) { | |
| scoreCard.style.display = 'block'; | |
| // Color based on score | |
| scoreValue.className = 'score-value'; | |
| credibilityBadge.className = 'credibility-badge'; | |
| if (score >= 0.7) { | |
| scoreValue.classList.add('score-high'); | |
| credibilityBadge.classList.add('badge-high'); | |
| credibilityBadge.textContent = '✓ Crédibilité Élevée'; | |
| } else if (score >= 0.4) { | |
| scoreValue.classList.add('score-medium'); | |
| credibilityBadge.classList.add('badge-medium'); | |
| credibilityBadge.textContent = '⚠ Crédibilité Moyenne'; | |
| } else { | |
| scoreValue.classList.add('score-low'); | |
| credibilityBadge.classList.add('badge-low'); | |
| credibilityBadge.textContent = '✗ Crédibilité Faible'; | |
| } | |
| } else { | |
| // Hide score card for text queries as requested | |
| scoreCard.style.display = 'none'; | |
| } | |
| // Summary | |
| summary.textContent = data.resumeAnalyse || 'Aucun résumé disponible'; | |
| // Build details HTML | |
| let detailsHTML = ''; | |
| // Source reputation from rule analysis | |
| const ruleResults = data.reglesAppliquees || {}; | |
| const sourceAnalysis = ruleResults.source_analysis || {}; | |
| if (sourceAnalysis.reputation) { | |
| const repColor = sourceAnalysis.reputation === 'High' ? '#22c55e' : | |
| sourceAnalysis.reputation === 'Low' ? '#ef4444' : '#eab308'; | |
| detailsHTML += ` | |
| <div class="detail-card"> | |
| <div class="detail-label">🏛️ Réputation Source</div> | |
| <div class="detail-value" style="color: ${repColor}">${sourceAnalysis.reputation}</div> | |
| </div> | |
| `; | |
| } | |
| if (sourceAnalysis.domain_age_days) { | |
| const years = (sourceAnalysis.domain_age_days / 365).toFixed(1); | |
| detailsHTML += ` | |
| <div class="detail-card"> | |
| <div class="detail-label">📅 Âge du Domaine</div> | |
| <div class="detail-value">${years} ans</div> | |
| </div> | |
| `; | |
| } | |
| // NLP analysis | |
| const nlpAnalysis = data.analyseNLP || {}; | |
| if (nlpAnalysis.sentiment) { | |
| detailsHTML += ` | |
| <div class="detail-card"> | |
| <div class="detail-label">💭 Sentiment</div> | |
| <div class="detail-value">${nlpAnalysis.sentiment.label} (${(nlpAnalysis.sentiment.score * 100).toFixed(0)}%)</div> | |
| </div> | |
| `; | |
| } | |
| if (nlpAnalysis.coherence_score !== null && nlpAnalysis.coherence_score !== undefined) { | |
| detailsHTML += ` | |
| <div class="detail-card"> | |
| <div class="detail-label">📊 Cohérence</div> | |
| <div class="detail-value">${(nlpAnalysis.coherence_score * 100).toFixed(0)}%</div> | |
| </div> | |
| `; | |
| } | |
| // Add PageRank if available | |
| if (data.pageRankEstimation && data.pageRankEstimation.estimatedPR) { | |
| detailsHTML += ` | |
| <div class="detail-card"> | |
| <div class="detail-label">📈 PageRank Estimé</div> | |
| <div class="detail-value">${data.pageRankEstimation.estimatedPR.toFixed(3)}</div> | |
| </div> | |
| `; | |
| } | |
| // Add SEO score if available | |
| if (data.seoAnalysis && data.seoAnalysis.seoScore) { | |
| detailsHTML += ` | |
| <div class="detail-card"> | |
| <div class="detail-label">🔍 Score SEO</div> | |
| <div class="detail-value">${data.seoAnalysis.seoScore}</div> | |
| </div> | |
| `; | |
| } | |
| // Add Bias Analysis if available | |
| if (nlpAnalysis.bias_analysis && nlpAnalysis.bias_analysis.score !== null) { | |
| const biasScore = nlpAnalysis.bias_analysis.score; | |
| const biasLabel = nlpAnalysis.bias_analysis.label || 'Non analysé'; | |
| const biasColor = biasScore > 0.5 ? '#ef4444' : biasScore > 0.3 ? '#eab308' : '#22c55e'; | |
| detailsHTML += ` | |
| <div class="detail-card"> | |
| <div class="detail-label">⚖️ Analyse de Biais <span class="help-icon" title="Mesure si le texte contient un langage biaisé ou partisan">?</span></div> | |
| <div class="detail-value" style="color: ${biasColor}">${biasLabel} (${(biasScore * 100).toFixed(0)}%)</div> | |
| </div> | |
| `; | |
| } | |
| // Fact checks | |
| const factChecks = ruleResults.fact_checking || []; | |
| if (factChecks.length > 0) { | |
| // Add a header for fact checks | |
| detailsHTML += ` | |
| <div style="grid-column: 1 / -1; margin-top: 1rem; margin-bottom: 0.5rem; font-weight: 600; color: #f472b6;"> | |
| 🕵️ Fact-Checks Trouvés (${factChecks.length}) | |
| </div> | |
| `; | |
| factChecks.forEach(fc => { | |
| detailsHTML += ` | |
| <div class="detail-card" style="grid-column: 1 / -1; border-color: rgba(244, 114, 182, 0.3);"> | |
| <div class="detail-label">🔍 ${fc.publisher || 'Source inconnue'}</div> | |
| <div class="detail-value" style="font-size: 1rem; margin-bottom: 0.5rem;">"${fc.claim}"</div> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <span style="color: #f472b6; font-weight: 700;">Verdict: ${fc.rating}</span> | |
| <a href="${fc.url}" target="_blank" style="color: #a855f7; text-decoration: none; font-size: 0.9rem;">Lire le rapport →</a> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| detailsGrid.innerHTML = detailsHTML; | |
| // ======================================== | |
| // DISPLAY NER ENTITIES | |
| // ======================================== | |
| const nerSection = document.getElementById('nerSection'); | |
| const nerEntities = document.getElementById('nerEntities'); | |
| const entities = nlpAnalysis.entities || data.ner_entities || []; | |
| if (entities && entities.length > 0) { | |
| nerSection.style.display = 'block'; | |
| let nerHTML = ''; | |
| const entityIcons = { | |
| 'PERSON': '👤', 'PER': '👤', | |
| 'ORG': '🏢', 'ORGANIZATION': '🏢', | |
| 'LOC': '📍', 'LOCATION': '📍', 'GPE': '📍', | |
| 'DATE': '📅', 'TIME': '🕐', | |
| 'MISC': '🏷️', 'PRODUCT': '📦', 'EVENT': '🎭' | |
| }; | |
| const entityClasses = { | |
| 'PERSON': 'person', 'PER': 'person', | |
| 'ORG': 'org', 'ORGANIZATION': 'org', | |
| 'LOC': 'location', 'LOCATION': 'location', 'GPE': 'location', | |
| 'DATE': 'date', 'TIME': 'date', | |
| 'MISC': 'misc', 'PRODUCT': 'misc', 'EVENT': 'misc' | |
| }; | |
| entities.forEach(entity => { | |
| const text = entity.text || entity.word || entity.entity; | |
| const type = (entity.label || entity.entity_group || entity.type || 'MISC').toUpperCase(); | |
| const icon = entityIcons[type] || '🏷️'; | |
| const cssClass = entityClasses[type] || 'misc'; | |
| nerHTML += ` | |
| <span class="ner-tag ${cssClass}"> | |
| <span class="ner-tag-icon">${icon}</span> | |
| <span class="ner-tag-text">${text}</span> | |
| <span class="ner-tag-type">${type}</span> | |
| </span> | |
| `; | |
| }); | |
| nerEntities.innerHTML = nerHTML; | |
| } else { | |
| nerSection.style.display = 'none'; | |
| } | |
| // ======================================== | |
| // DISPLAY E-E-A-T METRICS | |
| // ======================================== | |
| const eeatSection = document.getElementById('eeatSection'); | |
| const eeatData = data.eeat_score || data.eeatMetrics || null; | |
| if (eeatData) { | |
| eeatSection.style.display = 'block'; | |
| // Experience | |
| const experience = eeatData.experience || eeatData.Experience || 0; | |
| document.getElementById('eeatExperience').textContent = Math.round(experience * 100) + '%'; | |
| document.getElementById('eeatExperienceBar').style.width = (experience * 100) + '%'; | |
| // Expertise | |
| const expertise = eeatData.expertise || eeatData.Expertise || 0; | |
| document.getElementById('eeatExpertise').textContent = Math.round(expertise * 100) + '%'; | |
| document.getElementById('eeatExpertiseBar').style.width = (expertise * 100) + '%'; | |
| // Authority | |
| const authority = eeatData.authority || eeatData.Authority || 0; | |
| document.getElementById('eeatAuthority').textContent = Math.round(authority * 100) + '%'; | |
| document.getElementById('eeatAuthorityBar').style.width = (authority * 100) + '%'; | |
| // Trust | |
| const trust = eeatData.trust || eeatData.Trust || 0; | |
| document.getElementById('eeatTrust').textContent = Math.round(trust * 100) + '%'; | |
| document.getElementById('eeatTrustBar').style.width = (trust * 100) + '%'; | |
| // Overall E-E-A-T Score | |
| const overall = eeatData.overall || eeatData.Overall || | |
| ((experience * 0.15) + (expertise * 0.25) + (authority * 0.35) + (trust * 0.25)); | |
| document.getElementById('eeatOverall').innerHTML = | |
| `Score Global E-E-A-T: <strong>${Math.round(overall * 100)}%</strong>`; | |
| } else { | |
| eeatSection.style.display = 'none'; | |
| } | |
| // ======================================== | |
| // DISPLAY TREC IR METRICS | |
| // ======================================== | |
| const trecSection = document.getElementById('trecSection'); | |
| const trecData = data.trec_metrics || data.ir_metrics || nlpAnalysis.ir_metrics || null; | |
| if (trecData) { | |
| trecSection.style.display = 'block'; | |
| // Precision | |
| const precision = trecData.precision || trecData.P_10 || 0; | |
| document.getElementById('trecPrecision').textContent = | |
| typeof precision === 'number' ? (precision * 100).toFixed(1) + '%' : '--'; | |
| // Recall | |
| const recall = trecData.recall || trecData.recall_100 || 0; | |
| document.getElementById('trecRecall').textContent = | |
| typeof recall === 'number' ? (recall * 100).toFixed(1) + '%' : '--'; | |
| // MAP | |
| const map = trecData.map || trecData.MAP || 0; | |
| document.getElementById('trecMAP').textContent = | |
| typeof map === 'number' ? map.toFixed(3) : '--'; | |
| // NDCG | |
| const ndcg = trecData.ndcg || trecData.NDCG || 0; | |
| document.getElementById('trecNDCG').textContent = | |
| typeof ndcg === 'number' ? ndcg.toFixed(3) : '--'; | |
| // TF-IDF | |
| const tfidf = trecData.tfidf || trecData.tfidf_score || trecData.TF_IDF || 0; | |
| document.getElementById('trecTFIDF').textContent = | |
| typeof tfidf === 'number' ? tfidf.toFixed(3) : '--'; | |
| // MRR (Mean Reciprocal Rank) | |
| const mrr = trecData.mrr || trecData.recip_rank || trecData.MRR || 0; | |
| document.getElementById('trecMRR').textContent = | |
| typeof mrr === 'number' ? mrr.toFixed(3) : '--'; | |
| } else { | |
| trecSection.style.display = 'none'; | |
| } | |
| // ======================================== | |
| // DISPLAY "WHY THIS RESULT?" EXPLANATION | |
| // ======================================== | |
| const whySection = document.getElementById('whySection'); | |
| const whyContent = document.getElementById('whyContent'); | |
| // Generate explanations based on analysis data | |
| let whyItems = []; | |
| // Sentiment-based explanation | |
| if (nlpAnalysis.sentiment) { | |
| const sentLabel = nlpAnalysis.sentiment.label; | |
| const sentScore = (nlpAnalysis.sentiment.score * 100).toFixed(0); | |
| whyItems.push({ | |
| icon: sentLabel === 'POSITIVE' ? '😊' : (sentLabel === 'NEGATIVE' ? '😟' : '😐'), | |
| title: `Ton ${sentLabel.toLowerCase()}`, | |
| desc: `Le texte utilise un langage ${sentLabel === 'POSITIVE' ? 'positif et optimiste' : | |
| sentLabel === 'NEGATIVE' ? 'négatif ou critique' : 'neutre'} (${sentScore}% de confiance).` | |
| }); | |
| } | |
| // Coherence-based explanation | |
| if (nlpAnalysis.coherence_score !== undefined) { | |
| const cohScore = (nlpAnalysis.coherence_score * 100).toFixed(0); | |
| whyItems.push({ | |
| icon: nlpAnalysis.coherence_score > 0.7 ? '✓' : (nlpAnalysis.coherence_score > 0.4 ? '⚡' : '✗'), | |
| title: `Cohérence ${nlpAnalysis.coherence_score > 0.7 ? 'élevée' : (nlpAnalysis.coherence_score > 0.4 ? 'moyenne' : 'faible')}`, | |
| desc: `Le texte présente une cohérence sémantique de ${cohScore}%, indiquant ${ | |
| nlpAnalysis.coherence_score > 0.7 ? 'un contenu bien structuré et logique' : | |
| nlpAnalysis.coherence_score > 0.4 ? 'un contenu partiellement cohérent' : | |
| 'des incohérences ou contradictions possibles'}.` | |
| }); | |
| } | |
| // Source reputation | |
| if (sourceAnalysis.reputation) { | |
| whyItems.push({ | |
| icon: sourceAnalysis.reputation === 'High' ? '🏆' : (sourceAnalysis.reputation === 'Low' ? '⚠️' : '📊'), | |
| title: `Source ${sourceAnalysis.reputation === 'High' ? 'fiable' : | |
| sourceAnalysis.reputation === 'Low' ? 'douteuse' : 'moyenne'}`, | |
| desc: `La réputation de cette source est ${sourceAnalysis.reputation === 'High' ? | |
| 'reconnue et établie' : sourceAnalysis.reputation === 'Low' ? | |
| 'faible ou non vérifiable' : 'modérée'}.` | |
| }); | |
| } | |
| // Bias analysis | |
| if (nlpAnalysis.bias_analysis && nlpAnalysis.bias_analysis.score !== null) { | |
| const biasScore = nlpAnalysis.bias_analysis.score; | |
| whyItems.push({ | |
| icon: biasScore > 0.5 ? '⚖️' : '✓', | |
| title: biasScore > 0.5 ? 'Biais détecté' : 'Faible biais', | |
| desc: `${biasScore > 0.5 ? 'Le texte contient des éléments de langage partisan ou biaisé.' : | |
| 'Le texte semble relativement objectif et équilibré.'}` | |
| }); | |
| } | |
| // PageRank | |
| if (data.pageRankEstimation && data.pageRankEstimation.estimatedPR) { | |
| const pr = data.pageRankEstimation.estimatedPR; | |
| whyItems.push({ | |
| icon: '📈', | |
| title: 'Autorité de la page', | |
| desc: `PageRank estimé à ${pr.toFixed(3)}, basé sur l'analyse structurelle du site et des facteurs SEO.` | |
| }); | |
| } | |
| if (whyItems.length > 0) { | |
| whySection.style.display = 'block'; | |
| let whyHTML = ''; | |
| whyItems.forEach(item => { | |
| whyHTML += ` | |
| <div class="why-item"> | |
| <div class="why-icon">${item.icon}</div> | |
| <div class="why-text"> | |
| <div class="why-text-title">${item.title}</div> | |
| <div class="why-text-desc">${item.desc}</div> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| whyContent.innerHTML = whyHTML; | |
| } else { | |
| whySection.style.display = 'none'; | |
| } | |
| results.classList.add('visible'); | |
| // Fetch and render graph with slight delay to ensure DOM is ready | |
| requestAnimationFrame(() => { | |
| renderD3Graph(); | |
| }); | |
| } | |
| async function renderD3Graph() { | |
| logDebug("Starting renderD3Graph..."); | |
| const container = document.getElementById('cy'); | |
| // Check if D3 is loaded | |
| if (typeof d3 === 'undefined') { | |
| container.innerHTML = '<p class="error visible">Erreur: D3.js n\'a pas pu être chargé.</p>'; | |
| logDebug("ERROR: D3 undefined"); | |
| return; | |
| } | |
| try { | |
| container.innerHTML = '<div class="spinner"></div>'; // Loading state | |
| logDebug("Fetching graph data..."); | |
| const response = await fetch(`${API_URL}/api/ontology/graph`); | |
| const data = await response.json(); | |
| container.innerHTML = ''; // Clear loading | |
| logDebug(`Data received. Nodes: ${data.nodes ? data.nodes.length : 0}, Links: ${data.links ? data.links.length : 0}`); | |
| // FIX: Map backend data (label) to frontend expectations (name) | |
| if (data.nodes) { | |
| data.nodes = data.nodes.map(n => { | |
| n.name = n.name || n.label || 'Unknown'; | |
| if (!n.group) { | |
| if (n.type === 'Source') n.group = 1; | |
| else if (n.type === 'Entity') n.group = 1; | |
| else if (n.type === 'Claim') n.group = 2; | |
| else if (n.type === 'Evidence' && n.score > 0.7) n.group = 3; | |
| else if (n.type === 'Evidence') n.group = 4; | |
| else if (n.type === 'Metric') n.group = 3; | |
| else n.group = 2; | |
| } | |
| return n; | |
| }); | |
| } | |
| if (!data.nodes || data.nodes.length === 0) { | |
| container.innerHTML = '<p style="text-align:center; padding:2rem; color:#6b6b8a; width:100%; display:flex; justify-content:center; align-items:center; height:100%;">Aucune donnée ontologique disponible.</p>'; | |
| return; | |
| } | |
| // Get dimensions | |
| const width = container.clientWidth || 800; | |
| const height = container.clientHeight || 500; | |
| logDebug(`Container size: ${width}x${height}`); | |
| const svg = d3.select(container).append("svg") | |
| .attr("width", "100%") | |
| .attr("height", "100%") | |
| .attr("viewBox", [-width / 2, -height / 2, width, height]) | |
| .style("background-color", "rgba(0,0,0,0.2)"); // Visible background | |
| // ADDED: Overlay for details | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'nodeDetails'; | |
| overlay.className = 'node-details-overlay'; | |
| overlay.innerHTML = ` | |
| <button class="close-btn" onclick="document.getElementById('nodeDetails').classList.remove('visible')">×</button> | |
| <h3 id="detailTitle" style="color:#fff; margin-bottom:0.5rem; font-size:1.1rem; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:0.5rem;"></h3> | |
| <div id="detailBody" style="font-size:0.9rem; color:#ccc; line-height:1.5;"></div> | |
| `; | |
| container.appendChild(overlay); | |
| logDebug("SVG created. Starting simulation..."); | |
| // Colors: 1=Purple(Report), 2=Gray(Unknown), 3=Green(Good), 4=Red(Bad) | |
| const color = d3.scaleOrdinal() | |
| .domain([1, 2, 3, 4]) | |
| .range(["#8b5cf6", "#94a3b8", "#22c55e", "#ef4444"]); | |
| const simulation = d3.forceSimulation(data.nodes) | |
| .force("link", d3.forceLink(data.links).id(d => d.id).distance(120)) | |
| .force("charge", d3.forceManyBody().strength(-400)) | |
| .force("center", d3.forceCenter(0, 0)); | |
| // ADDED: Container click to close overlay | |
| svg.on("click", () => { | |
| document.getElementById('nodeDetails').classList.remove('visible'); | |
| node.attr("stroke", "#fff").attr("stroke-width", 1.5); | |
| }); | |
| // Arrow marker | |
| svg.append("defs").selectAll("marker") | |
| .data(["end"]) | |
| .join("marker") | |
| .attr("id", "arrow") | |
| .attr("viewBox", "0 -5 10 10") | |
| .attr("refX", 22) | |
| .attr("refY", 0) | |
| .attr("markerWidth", 6) | |
| .attr("markerHeight", 6) | |
| .attr("orient", "auto") | |
| .append("path") | |
| .attr("fill", "#64748b") | |
| .attr("d", "M0,-5L10,0L0,5"); | |
| const link = svg.append("g") | |
| .selectAll("line") | |
| .data(data.links) | |
| .join("line") | |
| .attr("stroke", "#475569") | |
| .attr("stroke-opacity", 0.6) | |
| .attr("stroke-width", 2) | |
| .attr("marker-end", "url(#arrow)"); | |
| const node = svg.append("g") | |
| .selectAll("circle") | |
| .data(data.nodes) | |
| .join("circle") | |
| .attr("r", d => d.group === 1 ? 18 : 8) | |
| .attr("fill", d => color(d.group)) | |
| .attr("stroke", "#fff") | |
| .attr("stroke-width", 1.5) | |
| .style("cursor", "pointer") | |
| .call(drag(simulation)) | |
| .on("click", (event, d) => { | |
| event.stopPropagation(); // Stop background click | |
| showNodeDetails(d); | |
| // Highlight selected | |
| node.attr("stroke", "#fff").attr("stroke-width", 1.5); | |
| d3.select(event.currentTarget).attr("stroke", "#f43f5e").attr("stroke-width", 3); | |
| }); | |
| // Labels | |
| const text = svg.append("g") | |
| .selectAll("text") | |
| .data(data.nodes) | |
| .join("text") | |
| .text(d => d.name.length > 20 ? d.name.substring(0, 20) + "..." : d.name) | |
| .attr("font-size", "11px") | |
| .attr("fill", "#e0e0e0") | |
| .attr("dx", 12) | |
| .attr("dy", 4) | |
| .style("pointer-events", "none") | |
| .style("text-shadow", "0 1px 2px black"); | |
| // Tooltip | |
| node.append("title").text(d => `${d.name}\n(${d.type})`); | |
| simulation.on("tick", () => { | |
| link | |
| .attr("x1", d => d.source.x) | |
| .attr("y1", d => d.source.y) | |
| .attr("x2", d => d.target.x) | |
| .attr("y2", d => d.target.y); | |
| node | |
| .attr("cx", d => d.x) | |
| .attr("cy", d => d.y); | |
| text | |
| .attr("x", d => d.x) | |
| .attr("y", d => d.y); | |
| }); | |
| // Zoom | |
| svg.call(d3.zoom().scaleExtent([0.1, 4]).on("zoom", (e) => { | |
| svg.selectAll('g').attr('transform', e.transform); | |
| })); | |
| logDebug("Graph rendered successfully."); | |
| } catch (err) { | |
| console.error("D3 Graph error:", err); | |
| const container = document.getElementById('cy'); | |
| if (container) container.innerHTML = `<p class="error visible">Erreur graphique: ${err.message}</p>`; | |
| logDebug(`ERROR EXCEPTION: ${err.message}`); | |
| } | |
| } | |
| function testD3() { | |
| logDebug("Starting Static Test..."); | |
| const container = document.getElementById('cy'); | |
| container.innerHTML = ''; | |
| const width = container.clientWidth || 800; | |
| const height = container.clientHeight || 500; | |
| logDebug(`Container: ${width}x${height}`); | |
| try { | |
| const svg = d3.select(container).append("svg") | |
| .attr("width", "100%") | |
| .attr("height", "100%") | |
| .attr("viewBox", [-width / 2, -height / 2, width, height]) | |
| .style("background-color", "#222"); | |
| svg.append("circle") | |
| .attr("r", 50) | |
| .attr("fill", "red") | |
| .attr("cx", 0) | |
| .attr("cy", 0); | |
| svg.append("text") | |
| .text("D3 WORKS") | |
| .attr("fill", "white") | |
| .attr("x", 0) | |
| .attr("y", 5) | |
| .attr("text-anchor", "middle"); | |
| logDebug("Static Test Complete. You should see a red circle."); | |
| } catch (e) { | |
| logDebug("Static Test ERROR: " + e.message); | |
| alert("Static Test Failed: " + e.message); | |
| } | |
| } | |
| // --- Helper Functions --- | |
| function logDebug(msg) { | |
| console.log(`[SysCRED Debug] ${msg}`); | |
| } | |
| function drag(simulation) { | |
| function dragstarted(event) { | |
| if (!event.active) simulation.alphaTarget(0.3).restart(); | |
| event.subject.fx = event.subject.x; | |
| event.subject.fy = event.subject.y; | |
| } | |
| function dragged(event) { | |
| event.subject.fx = event.x; | |
| event.subject.fy = event.y; | |
| } | |
| function dragended(event) { | |
| if (!event.active) simulation.alphaTarget(0); | |
| event.subject.fx = null; | |
| event.subject.fy = null; | |
| } | |
| return d3.drag() | |
| .on("start", dragstarted) | |
| .on("drag", dragged) | |
| .on("end", dragended); | |
| } | |
| function showNodeDetails(d) { | |
| const overlay = document.getElementById('nodeDetails'); | |
| const title = document.getElementById('detailTitle'); | |
| const body = document.getElementById('detailBody'); | |
| if(!overlay) return; | |
| title.textContent = d.name || d.label || 'Unknown'; | |
| let typeColor = "#94a3b8"; | |
| if(d.group === 1) typeColor = "#8b5cf6"; // Report | |
| if(d.group === 3) typeColor = "#22c55e"; // Good | |
| if(d.group === 4) typeColor = "#ef4444"; // Bad | |
| // Use uri field if available, fallback to id | |
| const displayUri = d.uri || d.id || 'N/A'; | |
| body.innerHTML = ` | |
| <div style="margin-bottom:0.5rem"> | |
| <span style="background:${typeColor}; color:white; padding:2px 6px; border-radius:4px; font-size:0.75rem;">${d.type || 'Unknown Type'}</span> | |
| </div> | |
| <div><strong>URI:</strong> <br><span style="font-family:monospace; color:#a855f7; word-break:break-all;">${displayUri}</span></div> | |
| ${d.score ? `<div style="margin-top:0.5rem"><strong>Score:</strong> ${(d.score * 100).toFixed(0)}%</div>` : ''} | |
| `; | |
| overlay.classList.add('visible'); | |
| } | |
| // Allow Enter key to trigger analysis | |
| document.getElementById('urlInput').addEventListener('keypress', function (e) { | |
| if (e.key === 'Enter') { | |
| analyzeUrl(); | |
| } | |
| }); | |
| // === EXPLAINABILITY DASHBOARD === | |
| let lastAnalysisData = null; // Store last analysis for explainer | |
| // Store data after analysis | |
| const originalDisplayResults = displayResults; | |
| displayResults = function(data) { | |
| lastAnalysisData = data; | |
| originalDisplayResults(data); | |
| }; | |
| function openExplainer() { | |
| if (!lastAnalysisData) { | |
| alert('Analysez d\'abord une URL pour voir les explications.'); | |
| return; | |
| } | |
| const modal = document.getElementById('explainerModal'); | |
| const body = document.getElementById('explainerBody'); | |
| const score = lastAnalysisData.scoreCredibilite || 0; | |
| const ruleResults = lastAnalysisData.reglesAppliquees || {}; | |
| const nlpAnalysis = lastAnalysisData.analyseNLP || {}; | |
| const sourceAnalysis = ruleResults.source_analysis || {}; | |
| // Determine verdict color and message | |
| let verdictColor, verdictText, verdictEmoji; | |
| if (score >= 0.7) { | |
| verdictColor = '#22c55e'; | |
| verdictText = 'Vous pouvez faire confiance à cette source.'; | |
| verdictEmoji = '✅'; | |
| } else if (score >= 0.4) { | |
| verdictColor = '#eab308'; | |
| verdictText = 'Soyez prudent, vérifiez avec d\'autres sources.'; | |
| verdictEmoji = '⚠️'; | |
| } else { | |
| verdictColor = '#ef4444'; | |
| verdictText = 'Méfiez-vous, cette source semble peu fiable.'; | |
| verdictEmoji = '❌'; | |
| } | |
| let html = ` | |
| <!-- Score Global --> | |
| <div class="metric-explain" style="border-left-color: ${verdictColor}; background: rgba(${verdictColor === '#22c55e' ? '34,197,94' : verdictColor === '#eab308' ? '234,179,8' : '239,68,68'}, 0.1);"> | |
| <div class="metric-explain-header"> | |
| <span class="metric-explain-icon">${verdictEmoji}</span> | |
| <span class="metric-explain-name">Score Global</span> | |
| <span class="metric-explain-value" style="color: ${verdictColor}; font-size: 1.5rem;">${(score * 100).toFixed(0)}%</span> | |
| </div> | |
| <div class="metric-explain-simple"> | |
| <strong>${verdictText}</strong><br><br> | |
| C'est comme une note à l'école :<br> | |
| • <span style="color:#22c55e">70-100%</span> = Excellent, source fiable<br> | |
| • <span style="color:#eab308">40-69%</span> = Moyen, à vérifier<br> | |
| • <span style="color:#ef4444">0-39%</span> = Faible, méfiance recommandée | |
| </div> | |
| <div class="verdict-bar"> | |
| <div class="verdict-fill" style="width: ${score * 100}%; background: ${verdictColor};"></div> | |
| </div> | |
| <div class="verdict-labels"> | |
| <span>⚠️ Pas fiable</span> | |
| <span>✅ Très fiable</span> | |
| </div> | |
| </div> | |
| `; | |
| // Reputation | |
| if (sourceAnalysis.reputation) { | |
| const rep = sourceAnalysis.reputation; | |
| const repEmoji = rep === 'High' ? '🏆' : rep === 'Medium' ? '👍' : rep === 'Low' ? '⚠️' : '❓'; | |
| const repText = rep === 'High' ? 'Source reconnue et respectée (ex: Le Monde, BBC)' | |
| : rep === 'Medium' ? 'Source correcte mais à vérifier' | |
| : rep === 'Low' ? 'Source peu fiable ou inconnue' | |
| : 'Nous ne connaissons pas cette source'; | |
| html += ` | |
| <div class="metric-explain"> | |
| <div class="metric-explain-header"> | |
| <span class="metric-explain-icon">${repEmoji}</span> | |
| <span class="metric-explain-name">Réputation de la Source</span> | |
| <span class="metric-explain-value">${rep}</span> | |
| </div> | |
| <div class="metric-explain-simple"> | |
| ${repText} | |
| </div> | |
| <div class="metric-explain-detail"> | |
| Nous comparons le site à une base de données de médias connus et vérifions s'il est cité par des journalistes. | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // Coherence | |
| if (nlpAnalysis.coherence_score !== undefined) { | |
| const coh = nlpAnalysis.coherence_score; | |
| const cohPercent = (coh * 100).toFixed(0); | |
| const cohEmoji = coh >= 0.6 ? '📝' : coh >= 0.4 ? '📄' : '❓'; | |
| html += ` | |
| <div class="metric-explain"> | |
| <div class="metric-explain-header"> | |
| <span class="metric-explain-icon">${cohEmoji}</span> | |
| <span class="metric-explain-name">Cohérence du Texte</span> | |
| <span class="metric-explain-value">${cohPercent}%</span> | |
| </div> | |
| <div class="metric-explain-simple"> | |
| Le texte est-il logique et bien écrit ?<br> | |
| Un texte bien structuré avec des phrases claires est généralement plus fiable. | |
| </div> | |
| <div class="metric-explain-detail"> | |
| Notre intelligence artificielle analyse si les phrases sont cohérentes entre elles et si le texte suit une logique. | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // PageRank | |
| if (lastAnalysisData.pageRankEstimation && lastAnalysisData.pageRankEstimation.estimatedPR) { | |
| const pr = lastAnalysisData.pageRankEstimation.estimatedPR; | |
| const prEmoji = pr >= 0.4 ? '🌟' : pr >= 0.2 ? '⭐' : '💫'; | |
| html += ` | |
| <div class="metric-explain"> | |
| <div class="metric-explain-header"> | |
| <span class="metric-explain-icon">${prEmoji}</span> | |
| <span class="metric-explain-name">PageRank (Popularité)</span> | |
| <span class="metric-explain-value">${pr.toFixed(3)}</span> | |
| </div> | |
| <div class="metric-explain-simple"> | |
| Plus un site est populaire et cité par d'autres sites importants, plus il est probablement fiable.<br> | |
| C'est comme le bouche-à-oreille : si beaucoup de gens recommandent quelqu'un, c'est bon signe. | |
| </div> | |
| <div class="metric-explain-detail"> | |
| Nous estimons combien de sites importants font des liens vers cette page (algorithme inspiré de Google). | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // SEO | |
| if (lastAnalysisData.seoAnalysis && lastAnalysisData.seoAnalysis.seoScore) { | |
| const seo = parseFloat(lastAnalysisData.seoAnalysis.seoScore); | |
| const seoEmoji = seo >= 0.7 ? '🔧' : seo >= 0.5 ? '🛠️' : '⚙️'; | |
| html += ` | |
| <div class="metric-explain"> | |
| <div class="metric-explain-header"> | |
| <span class="metric-explain-icon">${seoEmoji}</span> | |
| <span class="metric-explain-name">Qualité Technique (SEO)</span> | |
| <span class="metric-explain-value">${seo.toFixed(2)}</span> | |
| </div> | |
| <div class="metric-explain-simple"> | |
| Le site est-il bien construit ?<br> | |
| Un site professionnel bien structuré inspire plus confiance qu'un site mal fait. | |
| </div> | |
| <div class="metric-explain-detail"> | |
| Nous vérifions si le site a un titre, une description, des balises correctes, etc. | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // Fact-Checks | |
| const factChecks = ruleResults.fact_checking || []; | |
| html += ` | |
| <div class="metric-explain" style="border-left-color: #f472b6;"> | |
| <div class="metric-explain-header"> | |
| <span class="metric-explain-icon">🕵️</span> | |
| <span class="metric-explain-name">Vérification des Faits</span> | |
| <span class="metric-explain-value">${factChecks.length} trouvé(s)</span> | |
| </div> | |
| <div class="metric-explain-simple"> | |
| Nous cherchons si des fact-checkers professionnels ont déjà vérifié des informations similaires.<br> | |
| ${factChecks.length > 0 | |
| ? '✅ Des vérifications existent - consultez-les !' | |
| : 'Aucune vérification trouvée - cela ne veut pas dire que c\'est faux.'} | |
| </div> | |
| <div class="metric-explain-detail"> | |
| Source : Google Fact Check Tools API - vérifie auprès de PolitiFact, Snopes, AFP, etc. | |
| </div> | |
| </div> | |
| `; | |
| // How it's calculated | |
| html += ` | |
| <div style="margin-top: 1.5rem; padding: 1rem; background: rgba(124, 58, 237, 0.1); border-radius: 12px; border: 1px dashed rgba(124, 58, 237, 0.3);"> | |
| <strong style="color: #a855f7;">📊 Comment le score est calculé ?</strong> | |
| <p style="margin-top: 0.5rem; color: #8b8ba7; font-size: 0.9rem;"> | |
| Le score combine plusieurs facteurs :<br> | |
| • Réputation de la source (22%)<br> | |
| • Cohérence du texte (12%)<br> | |
| • Qualité technique (15%)<br> | |
| • Vérifications de faits (17%)<br> | |
| • Âge du domaine et autres (34%) | |
| </p> | |
| </div> | |
| `; | |
| body.innerHTML = html; | |
| modal.classList.add('visible'); | |
| } | |
| function closeExplainer() { | |
| document.getElementById('explainerModal').classList.remove('visible'); | |
| } | |
| // Close modal with Escape key | |
| document.addEventListener('keydown', function(e) { | |
| if (e.key === 'Escape') closeExplainer(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |