D Ф m i И i q ц e L Ф y e r
Fix: API URL detection & add missing NER/EEAT data
3f5e84d
<!DOCTYPE html>
<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>