D Ф m i И i q ц e L Ф y e r commited on
Commit
6059862
·
1 Parent(s): f816411

Update SysCRED v2.3.1

Browse files
Files changed (1) hide show
  1. syscred/static/index.html +469 -160
syscred/static/index.html CHANGED
@@ -17,6 +17,7 @@
17
  border: 1px solid rgba(255, 255, 255, 0.1);
18
  position: relative;
19
  display: block;
 
20
  }
21
 
22
  * {
@@ -118,12 +119,25 @@
118
  transform: none;
119
  }
120
 
121
- .results { display: none; }
122
- .results.visible { display: block; animation: fadeIn 0.5s ease; }
 
 
 
 
 
 
123
 
124
  @keyframes fadeIn {
125
- from { opacity: 0; transform: translateY(20px); }
126
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
127
  }
128
 
129
  .score-card {
@@ -136,11 +150,29 @@
136
  margin-bottom: 2rem;
137
  }
138
 
139
- .score-value { font-size: 4rem; font-weight: 700; margin: 1rem 0; }
140
- .score-high { color: #22c55e; }
141
- .score-medium { color: #eab308; }
142
- .score-low { color: #ef4444; }
143
- .score-label { font-size: 1.2rem; color: #8b8ba7; margin-bottom: 1rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  .credibility-badge {
146
  display: inline-block;
@@ -152,9 +184,23 @@
152
  letter-spacing: 1px;
153
  }
154
 
155
- .badge-high { background: rgba(34, 197, 94, 0.2); color: #22c55e; border: 1px solid #22c55e; }
156
- .badge-medium { background: rgba(234, 179, 8, 0.2); color: #eab308; border: 1px solid #eab308; }
157
- .badge-low { background: rgba(239, 68, 68, 0.2); color: #ef4444; border: 1px solid #ef4444; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  .details-grid {
160
  display: grid;
@@ -170,8 +216,17 @@
170
  padding: 1.5rem;
171
  }
172
 
173
- .detail-label { font-size: 0.85rem; color: #8b8ba7; margin-bottom: 0.5rem; }
174
- .detail-value { font-size: 1.1rem; font-weight: 600; color: #fff; }
 
 
 
 
 
 
 
 
 
175
 
176
  .summary-box {
177
  background: rgba(124, 58, 237, 0.1);
@@ -181,10 +236,21 @@
181
  margin-bottom: 2rem;
182
  }
183
 
184
- .summary-title { font-weight: 600; margin-bottom: 0.5rem; color: #a855f7; }
 
 
 
 
185
 
186
- .loading { text-align: center; padding: 3rem; display: none; }
187
- .loading.visible { display: block; }
 
 
 
 
 
 
 
188
 
189
  .spinner {
190
  width: 50px;
@@ -196,7 +262,11 @@
196
  margin: 0 auto 1rem;
197
  }
198
 
199
- @keyframes spin { to { transform: rotate(360deg); } }
 
 
 
 
200
 
201
  .error {
202
  background: rgba(239, 68, 68, 0.1);
@@ -206,50 +276,63 @@
206
  color: #ef4444;
207
  display: none;
208
  }
209
- .error.visible { display: block; }
210
 
211
- footer { text-align: center; margin-top: 3rem; color: #6b6b8a; font-size: 0.9rem; }
212
- footer a { color: #7c3aed; text-decoration: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
- /* Backend Toggle Switch */
215
- .backend-toggle {
216
- display: flex;
217
- align-items: center;
218
- justify-content: center;
219
- gap: 0.75rem;
220
- margin-top: 1rem;
221
- padding: 0.75rem;
222
- background: rgba(0,0,0,0.2);
223
- border-radius: 10px;
224
- }
225
- .backend-toggle label { font-size: 0.85rem; color: #8b8ba7; cursor: pointer; }
226
- .backend-toggle .active { color: #a855f7; font-weight: 600; }
227
- .toggle-switch { position: relative; width: 50px; height: 26px; }
228
- .toggle-switch input { opacity: 0; width: 0; height: 0; }
229
- .toggle-slider {
230
  position: absolute;
231
- cursor: pointer;
232
- top: 0; left: 0; right: 0; bottom: 0;
233
- background: linear-gradient(135deg, #22c55e, #16a34a);
234
- border-radius: 26px;
235
- transition: 0.3s;
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- .toggle-slider:before {
238
  position: absolute;
239
- content: '';
240
- height: 20px;
241
- width: 20px;
242
- left: 3px;
243
- bottom: 3px;
244
- background: white;
245
- border-radius: 50%;
246
- transition: 0.3s;
 
 
 
 
 
 
 
 
 
247
  }
248
- .toggle-switch input:checked + .toggle-slider { background: linear-gradient(135deg, #7c3aed, #a855f7); }
249
- .toggle-switch input:checked + .toggle-slider:before { transform: translateX(24px); }
250
- .backend-status { font-size: 0.75rem; color: #6b6b8a; text-align: center; margin-top: 0.5rem; }
251
- .backend-status.local { color: #22c55e; }
252
- .backend-status.remote { color: #a855f7; }
253
  </style>
254
  </head>
255
 
@@ -262,20 +345,12 @@
262
 
263
  <div class="search-box">
264
  <div class="input-group">
265
- <input type="text" id="urlInput" placeholder="Entrez une URL ou du texte à analyser" autofocus>
266
- <button id="analyzeBtn" onclick="analyzeUrl()">🔍 Analyser</button>
 
 
 
267
  </div>
268
-
269
- <!-- Backend Toggle -->
270
- <div class="backend-toggle">
271
- <label id="labelLocal" class="active">🖥️ Local</label>
272
- <div class="toggle-switch">
273
- <input type="checkbox" id="backendToggle" onchange="toggleBackend()">
274
- <span class="toggle-slider"></span>
275
- </div>
276
- <label id="labelRemote">☁️ HF Space</label>
277
- </div>
278
- <div class="backend-status local" id="backendStatus">Backend: localhost:5001 (léger, sans ML)</div>
279
  </div>
280
 
281
  <div class="loading" id="loading">
@@ -300,58 +375,25 @@
300
  <div class="details-grid" id="detailsGrid"></div>
301
 
302
  <div class="graph-section" style="margin-top: 3rem;">
303
- <div class="summary-title" style="margin-bottom: 2rem; color: #60a5fa;">🕸️ Réseau Neuro-Symbolique (Ontologie)</div>
 
 
 
 
 
304
  <div id="cy" class="graph-container"></div>
305
  </div>
306
  </div>
307
 
308
  <footer>
309
- <p>SysCRED v2.3.1 - Prototype de recherche doctorale</p>
310
- <p>© Dominique S. Loyer - UQAM | <a href="https://doi.org/10.5281/zenodo.17943226" target="_blank">DOI: 10.5281/zenodo.17943226</a></p>
 
311
  </footer>
312
  </div>
313
 
314
  <script>
315
- // Backend URLs - Auto-detect or manual toggle
316
- const LOCAL_API_URL = 'http://localhost:5001';
317
- const REMOTE_API_URL = ''; // Empty = same origin for HF Space
318
-
319
- // Auto-detect environment on load
320
- let API_URL = (() => {
321
- const hostname = window.location.hostname;
322
- if (hostname.includes('hf.space') || hostname.includes('huggingface.co')) {
323
- // On HF Space, use same origin
324
- document.getElementById('backendToggle').checked = true;
325
- document.getElementById('labelLocal').classList.remove('active');
326
- document.getElementById('labelRemote').classList.add('active');
327
- document.getElementById('backendStatus').textContent = 'Backend: HF Space (ML complet)';
328
- document.getElementById('backendStatus').className = 'backend-status remote';
329
- return '';
330
- }
331
- return LOCAL_API_URL;
332
- })();
333
-
334
- function toggleBackend() {
335
- const toggle = document.getElementById('backendToggle');
336
- const status = document.getElementById('backendStatus');
337
- const labelLocal = document.getElementById('labelLocal');
338
- const labelRemote = document.getElementById('labelRemote');
339
-
340
- if (toggle.checked) {
341
- API_URL = REMOTE_API_URL || 'https://domloyer-syscred.hf.space';
342
- status.textContent = 'Backend: HF Space (ML complet, plus lent)';
343
- status.className = 'backend-status remote';
344
- labelLocal.classList.remove('active');
345
- labelRemote.classList.add('active');
346
- } else {
347
- API_URL = LOCAL_API_URL;
348
- status.textContent = 'Backend: localhost:5001 (léger, sans ML)';
349
- status.className = 'backend-status local';
350
- labelLocal.classList.add('active');
351
- labelRemote.classList.remove('active');
352
- }
353
- console.log('[SysCRED] Backend switched to:', API_URL || '(same origin)');
354
- }
355
 
356
  async function analyzeUrl() {
357
  const urlInput = document.getElementById('urlInput');
@@ -361,8 +403,13 @@
361
  const btn = document.getElementById('analyzeBtn');
362
 
363
  const inputData = urlInput.value.trim();
364
- if (!inputData) { alert('Veuillez entrer une URL ou du texte'); return; }
365
 
 
 
 
 
 
 
366
  results.classList.remove('visible');
367
  error.classList.remove('visible');
368
  loading.classList.add('visible');
@@ -371,12 +418,22 @@
371
  try {
372
  const response = await fetch(`${API_URL}/api/verify`, {
373
  method: 'POST',
374
- headers: { 'Content-Type': 'application/json' },
375
- body: JSON.stringify({ input_data: inputData, include_seo: true, include_pagerank: true })
 
 
 
 
 
 
376
  });
377
 
378
  const data = await response.json();
379
- if (!response.ok) throw new Error(data.error || 'Erreur lors de l\'analyse');
 
 
 
 
380
  displayResults(data);
381
 
382
  } catch (err) {
@@ -395,14 +452,17 @@
395
  const summary = document.getElementById('summary');
396
  const detailsGrid = document.getElementById('detailsGrid');
397
 
 
398
  const score = data.scoreCredibilite || 0;
399
  scoreValue.textContent = score.toFixed(2);
400
 
 
401
  const isUrl = data.informationEntree && data.informationEntree.startsWith('http');
402
  const scoreCard = document.querySelector('.score-card');
403
 
404
  if (isUrl) {
405
  scoreCard.style.display = 'block';
 
406
  scoreValue.className = 'score-value';
407
  credibilityBadge.className = 'credibility-badge';
408
 
@@ -420,121 +480,370 @@
420
  credibilityBadge.textContent = '✗ Crédibilité Faible';
421
  }
422
  } else {
 
423
  scoreCard.style.display = 'none';
424
  }
425
 
 
426
  summary.textContent = data.resumeAnalyse || 'Aucun résumé disponible';
427
 
 
428
  let detailsHTML = '';
 
 
429
  const ruleResults = data.reglesAppliquees || {};
430
  const sourceAnalysis = ruleResults.source_analysis || {};
431
 
432
  if (sourceAnalysis.reputation) {
433
- const repColor = sourceAnalysis.reputation === 'High' ? '#22c55e' : sourceAnalysis.reputation === 'Low' ? '#ef4444' : '#eab308';
434
- detailsHTML += `<div class="detail-card"><div class="detail-label">🏛️ Réputation Source</div><div class="detail-value" style="color: ${repColor}">${sourceAnalysis.reputation}</div></div>`;
 
 
 
 
 
 
435
  }
436
 
437
  if (sourceAnalysis.domain_age_days) {
438
  const years = (sourceAnalysis.domain_age_days / 365).toFixed(1);
439
- detailsHTML += `<div class="detail-card"><div class="detail-label">📅 Âge du Domaine</div><div class="detail-value">${years} ans</div></div>`;
 
 
 
 
 
440
  }
441
 
 
442
  const nlpAnalysis = data.analyseNLP || {};
 
443
  if (nlpAnalysis.sentiment) {
444
- 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>`;
 
 
 
 
 
445
  }
 
446
  if (nlpAnalysis.coherence_score !== null && nlpAnalysis.coherence_score !== undefined) {
447
- detailsHTML += `<div class="detail-card"><div class="detail-label">📊 Cohérence</div><div class="detail-value">${(nlpAnalysis.coherence_score * 100).toFixed(0)}%</div></div>`;
 
 
 
 
 
448
  }
449
 
 
450
  if (data.pageRankEstimation && data.pageRankEstimation.estimatedPR) {
451
- detailsHTML += `<div class="detail-card"><div class="detail-label">📈 PageRank Estimé</div><div class="detail-value">${data.pageRankEstimation.estimatedPR.toFixed(3)}</div></div>`;
 
 
 
 
 
452
  }
 
 
453
  if (data.seoAnalysis && data.seoAnalysis.seoScore) {
454
- detailsHTML += `<div class="detail-card"><div class="detail-label">🔍 Score SEO</div><div class="detail-value">${data.seoAnalysis.seoScore}</div></div>`;
 
 
 
 
 
455
  }
456
 
 
457
  const factChecks = ruleResults.fact_checking || [];
458
  if (factChecks.length > 0) {
459
- 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>`;
 
 
 
 
 
 
460
  factChecks.forEach(fc => {
461
- 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>`;
 
 
 
 
 
 
 
 
 
462
  });
463
  }
464
 
465
  detailsGrid.innerHTML = detailsHTML;
 
466
  results.classList.add('visible');
467
- requestAnimationFrame(() => renderD3Graph());
 
 
 
 
468
  }
469
 
470
  async function renderD3Graph() {
 
471
  const container = document.getElementById('cy');
472
- if (typeof d3 === 'undefined') { container.innerHTML = '<p class="error visible">Erreur: D3.js non chargé.</p>'; return; }
 
 
 
 
 
 
473
 
474
  try {
475
- container.innerHTML = '<div class="spinner"></div>';
 
 
476
  const response = await fetch(`${API_URL}/api/ontology/graph`);
477
  const data = await response.json();
478
- container.innerHTML = '';
 
 
479
 
480
  if (!data.nodes || data.nodes.length === 0) {
481
- container.innerHTML = '<p style="text-align:center; padding:2rem; color:#6b6b8a;">Aucune donnée ontologique disponible.</p>';
482
  return;
483
  }
484
 
 
485
  const width = container.clientWidth || 800;
486
  const height = container.clientHeight || 500;
 
487
 
488
  const svg = d3.select(container).append("svg")
489
- .attr("width", "100%").attr("height", "100%")
 
490
  .attr("viewBox", [-width / 2, -height / 2, width, height])
491
- .style("background-color", "rgba(0,0,0,0.2)");
492
-
493
- const color = d3.scaleOrdinal().domain([1, 2, 3, 4]).range(["#8b5cf6", "#94a3b8", "#22c55e", "#ef4444"]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
 
495
  const simulation = d3.forceSimulation(data.nodes)
496
  .force("link", d3.forceLink(data.links).id(d => d.id).distance(120))
497
  .force("charge", d3.forceManyBody().strength(-400))
498
  .force("center", d3.forceCenter(0, 0));
 
 
 
 
 
 
499
 
500
- svg.append("defs").selectAll("marker").data(["end"]).join("marker")
501
- .attr("id", "arrow").attr("viewBox", "0 -5 10 10").attr("refX", 22).attr("refY", 0)
502
- .attr("markerWidth", 6).attr("markerHeight", 6).attr("orient", "auto")
503
- .append("path").attr("fill", "#64748b").attr("d", "M0,-5L10,0L0,5");
504
-
505
- const link = svg.append("g").selectAll("line").data(data.links).join("line")
506
- .attr("stroke", "#475569").attr("stroke-opacity", 0.6).attr("stroke-width", 2).attr("marker-end", "url(#arrow)");
507
-
508
- const node = svg.append("g").selectAll("circle").data(data.nodes).join("circle")
509
- .attr("r", d => d.group === 1 ? 18 : 8).attr("fill", d => color(d.group))
510
- .attr("stroke", "#fff").attr("stroke-width", 1.5).style("cursor", "pointer")
511
- .call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended));
512
-
513
- const text = svg.append("g").selectAll("text").data(data.nodes).join("text")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  .text(d => d.name.length > 20 ? d.name.substring(0, 20) + "..." : d.name)
515
- .attr("font-size", "11px").attr("fill", "#e0e0e0").attr("dx", 12).attr("dy", 4)
516
- .style("pointer-events", "none").style("text-shadow", "0 1px 2px black");
517
-
 
 
 
 
 
518
  node.append("title").text(d => `${d.name}\n(${d.type})`);
519
 
520
  simulation.on("tick", () => {
521
- 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);
522
- node.attr("cx", d => d.x).attr("cy", d => d.y);
523
- text.attr("x", d => d.x).attr("y", d => d.y);
 
 
 
 
 
 
 
 
 
 
524
  });
525
 
526
- svg.call(d3.zoom().scaleExtent([0.1, 4]).on("zoom", (e) => svg.selectAll('g').attr('transform', e.transform)));
 
 
 
527
 
528
- function dragstarted(event) { if (!event.active) simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; }
529
- function dragged(event) { event.subject.fx = event.x; event.subject.fy = event.y; }
530
- function dragended(event) { if (!event.active) simulation.alphaTarget(0); event.subject.fx = null; event.subject.fy = null; }
531
 
532
  } catch (err) {
533
- container.innerHTML = `<p class="error visible">Erreur graphique: ${err.message}</p>`;
 
 
 
534
  }
535
  }
536
 
537
- document.getElementById('urlInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') analyzeUrl(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  </script>
539
  </body>
540
 
 
17
  border: 1px solid rgba(255, 255, 255, 0.1);
18
  position: relative;
19
  display: block;
20
+ /* Force display */
21
  }
22
 
23
  * {
 
119
  transform: none;
120
  }
121
 
122
+ .results {
123
+ display: none;
124
+ }
125
+
126
+ .results.visible {
127
+ display: block;
128
+ animation: fadeIn 0.5s ease;
129
+ }
130
 
131
  @keyframes fadeIn {
132
+ from {
133
+ opacity: 0;
134
+ transform: translateY(20px);
135
+ }
136
+
137
+ to {
138
+ opacity: 1;
139
+ transform: translateY(0);
140
+ }
141
  }
142
 
143
  .score-card {
 
150
  margin-bottom: 2rem;
151
  }
152
 
153
+ .score-value {
154
+ font-size: 4rem;
155
+ font-weight: 700;
156
+ margin: 1rem 0;
157
+ }
158
+
159
+ .score-high {
160
+ color: #22c55e;
161
+ }
162
+
163
+ .score-medium {
164
+ color: #eab308;
165
+ }
166
+
167
+ .score-low {
168
+ color: #ef4444;
169
+ }
170
+
171
+ .score-label {
172
+ font-size: 1.2rem;
173
+ color: #8b8ba7;
174
+ margin-bottom: 1rem;
175
+ }
176
 
177
  .credibility-badge {
178
  display: inline-block;
 
184
  letter-spacing: 1px;
185
  }
186
 
187
+ .badge-high {
188
+ background: rgba(34, 197, 94, 0.2);
189
+ color: #22c55e;
190
+ border: 1px solid #22c55e;
191
+ }
192
+
193
+ .badge-medium {
194
+ background: rgba(234, 179, 8, 0.2);
195
+ color: #eab308;
196
+ border: 1px solid #eab308;
197
+ }
198
+
199
+ .badge-low {
200
+ background: rgba(239, 68, 68, 0.2);
201
+ color: #ef4444;
202
+ border: 1px solid #ef4444;
203
+ }
204
 
205
  .details-grid {
206
  display: grid;
 
216
  padding: 1.5rem;
217
  }
218
 
219
+ .detail-label {
220
+ font-size: 0.85rem;
221
+ color: #8b8ba7;
222
+ margin-bottom: 0.5rem;
223
+ }
224
+
225
+ .detail-value {
226
+ font-size: 1.1rem;
227
+ font-weight: 600;
228
+ color: #fff;
229
+ }
230
 
231
  .summary-box {
232
  background: rgba(124, 58, 237, 0.1);
 
236
  margin-bottom: 2rem;
237
  }
238
 
239
+ .summary-title {
240
+ font-weight: 600;
241
+ margin-bottom: 0.5rem;
242
+ color: #a855f7;
243
+ }
244
 
245
+ .loading {
246
+ text-align: center;
247
+ padding: 3rem;
248
+ display: none;
249
+ }
250
+
251
+ .loading.visible {
252
+ display: block;
253
+ }
254
 
255
  .spinner {
256
  width: 50px;
 
262
  margin: 0 auto 1rem;
263
  }
264
 
265
+ @keyframes spin {
266
+ to {
267
+ transform: rotate(360deg);
268
+ }
269
+ }
270
 
271
  .error {
272
  background: rgba(239, 68, 68, 0.1);
 
276
  color: #ef4444;
277
  display: none;
278
  }
 
279
 
280
+ .error.visible {
281
+ display: block;
282
+ }
283
+
284
+ footer {
285
+ text-align: center;
286
+ margin-top: 3rem;
287
+ color: #6b6b8a;
288
+ font-size: 0.9rem;
289
+ }
290
+
291
+ footer a {
292
+ color: #7c3aed;
293
+ text-decoration: none;
294
+ }
295
 
296
+ /* Node Details Overlay */
297
+ .node-details-overlay {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  position: absolute;
299
+ top: 20px;
300
+ right: 20px;
301
+ background: rgba(15, 15, 35, 0.95);
302
+ border: 1px solid rgba(124, 58, 237, 0.3);
303
+ border-radius: 12px;
304
+ padding: 1.5rem;
305
+ width: 300px;
306
+ display: none;
307
+ backdrop-filter: blur(10px);
308
+ z-index: 100;
309
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
310
+ pointer-events: auto;
311
+ }
312
+ .node-details-overlay.visible {
313
+ display: block;
314
+ animation: fadeIn 0.3s ease;
315
  }
316
+ .close-btn {
317
  position: absolute;
318
+ top: 10px;
319
+ right: 15px;
320
+ background: none;
321
+ border: none;
322
+ color: #8b8ba7;
323
+ font-size: 1.5rem;
324
+ cursor: pointer;
325
+ padding: 0;
326
+ line-height: 1;
327
+ width: auto;
328
+ height: auto;
329
+ box-shadow: none;
330
+ }
331
+ .close-btn:hover {
332
+ color: #fff;
333
+ transform: none;
334
+ box-shadow: none;
335
  }
 
 
 
 
 
336
  </style>
337
  </head>
338
 
 
345
 
346
  <div class="search-box">
347
  <div class="input-group">
348
+ <input type="text" id="urlInput" placeholder="Entrez une URL à analyser (ex: https://www.lemonde.fr)"
349
+ autofocus>
350
+ <button id="analyzeBtn" onclick="analyzeUrl()">
351
+ 🔍 Analyser
352
+ </button>
353
  </div>
 
 
 
 
 
 
 
 
 
 
 
354
  </div>
355
 
356
  <div class="loading" id="loading">
 
375
  <div class="details-grid" id="detailsGrid"></div>
376
 
377
  <div class="graph-section" style="margin-top: 3rem;">
378
+ <div class="summary-title" style="margin-bottom: 2rem; color: #60a5fa;">🕸️ Réseau Neuro-Symbolique
379
+ (Ontologie)</div>
380
+ <!-- Debug link -->
381
+ <small style="color: #666; cursor: pointer;"
382
+ onclick="alert('D3 Loaded: ' + (typeof d3 !== 'undefined'))">Debug: Vérifier D3</small>
383
+
384
  <div id="cy" class="graph-container"></div>
385
  </div>
386
  </div>
387
 
388
  <footer>
389
+ <p>SysCRED v2.0 - Prototype de recherche doctorale</p>
390
+ <p>© Dominique S. Loyer - UQAM | <a href="https://doi.org/10.5281/zenodo.17943226" target="_blank">DOI:
391
+ 10.5281/zenodo.17943226</a></p>
392
  </footer>
393
  </div>
394
 
395
  <script>
396
+ const API_URL = 'http://localhost:5001';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
  async function analyzeUrl() {
399
  const urlInput = document.getElementById('urlInput');
 
403
  const btn = document.getElementById('analyzeBtn');
404
 
405
  const inputData = urlInput.value.trim();
 
406
 
407
+ if (!inputData) {
408
+ alert('Veuillez entrer une URL');
409
+ return;
410
+ }
411
+
412
+ // Reset UI
413
  results.classList.remove('visible');
414
  error.classList.remove('visible');
415
  loading.classList.add('visible');
 
418
  try {
419
  const response = await fetch(`${API_URL}/api/verify`, {
420
  method: 'POST',
421
+ headers: {
422
+ 'Content-Type': 'application/json',
423
+ },
424
+ body: JSON.stringify({
425
+ input_data: inputData,
426
+ include_seo: true,
427
+ include_pagerank: true
428
+ })
429
  });
430
 
431
  const data = await response.json();
432
+
433
+ if (!response.ok) {
434
+ throw new Error(data.error || 'Erreur lors de l\'analyse');
435
+ }
436
+
437
  displayResults(data);
438
 
439
  } catch (err) {
 
452
  const summary = document.getElementById('summary');
453
  const detailsGrid = document.getElementById('detailsGrid');
454
 
455
+ // Score
456
  const score = data.scoreCredibilite || 0;
457
  scoreValue.textContent = score.toFixed(2);
458
 
459
+ // Conditional Display: Hide Score Card if TEXT input, show if URL
460
  const isUrl = data.informationEntree && data.informationEntree.startsWith('http');
461
  const scoreCard = document.querySelector('.score-card');
462
 
463
  if (isUrl) {
464
  scoreCard.style.display = 'block';
465
+ // Color based on score
466
  scoreValue.className = 'score-value';
467
  credibilityBadge.className = 'credibility-badge';
468
 
 
480
  credibilityBadge.textContent = '✗ Crédibilité Faible';
481
  }
482
  } else {
483
+ // Hide score card for text queries as requested
484
  scoreCard.style.display = 'none';
485
  }
486
 
487
+ // Summary
488
  summary.textContent = data.resumeAnalyse || 'Aucun résumé disponible';
489
 
490
+ // Build details HTML
491
  let detailsHTML = '';
492
+
493
+ // Source reputation from rule analysis
494
  const ruleResults = data.reglesAppliquees || {};
495
  const sourceAnalysis = ruleResults.source_analysis || {};
496
 
497
  if (sourceAnalysis.reputation) {
498
+ const repColor = sourceAnalysis.reputation === 'High' ? '#22c55e' :
499
+ sourceAnalysis.reputation === 'Low' ? '#ef4444' : '#eab308';
500
+ detailsHTML += `
501
+ <div class="detail-card">
502
+ <div class="detail-label">🏛️ Réputation Source</div>
503
+ <div class="detail-value" style="color: ${repColor}">${sourceAnalysis.reputation}</div>
504
+ </div>
505
+ `;
506
  }
507
 
508
  if (sourceAnalysis.domain_age_days) {
509
  const years = (sourceAnalysis.domain_age_days / 365).toFixed(1);
510
+ detailsHTML += `
511
+ <div class="detail-card">
512
+ <div class="detail-label">📅 Âge du Domaine</div>
513
+ <div class="detail-value">${years} ans</div>
514
+ </div>
515
+ `;
516
  }
517
 
518
+ // NLP analysis
519
  const nlpAnalysis = data.analyseNLP || {};
520
+
521
  if (nlpAnalysis.sentiment) {
522
+ detailsHTML += `
523
+ <div class="detail-card">
524
+ <div class="detail-label">💭 Sentiment</div>
525
+ <div class="detail-value">${nlpAnalysis.sentiment.label} (${(nlpAnalysis.sentiment.score * 100).toFixed(0)}%)</div>
526
+ </div>
527
+ `;
528
  }
529
+
530
  if (nlpAnalysis.coherence_score !== null && nlpAnalysis.coherence_score !== undefined) {
531
+ detailsHTML += `
532
+ <div class="detail-card">
533
+ <div class="detail-label">📊 Cohérence</div>
534
+ <div class="detail-value">${(nlpAnalysis.coherence_score * 100).toFixed(0)}%</div>
535
+ </div>
536
+ `;
537
  }
538
 
539
+ // Add PageRank if available
540
  if (data.pageRankEstimation && data.pageRankEstimation.estimatedPR) {
541
+ detailsHTML += `
542
+ <div class="detail-card">
543
+ <div class="detail-label">📈 PageRank Estimé</div>
544
+ <div class="detail-value">${data.pageRankEstimation.estimatedPR.toFixed(3)}</div>
545
+ </div>
546
+ `;
547
  }
548
+
549
+ // Add SEO score if available
550
  if (data.seoAnalysis && data.seoAnalysis.seoScore) {
551
+ detailsHTML += `
552
+ <div class="detail-card">
553
+ <div class="detail-label">🔍 Score SEO</div>
554
+ <div class="detail-value">${data.seoAnalysis.seoScore}</div>
555
+ </div>
556
+ `;
557
  }
558
 
559
+ // Fact checks
560
  const factChecks = ruleResults.fact_checking || [];
561
  if (factChecks.length > 0) {
562
+ // Add a header for fact checks
563
+ detailsHTML += `
564
+ <div style="grid-column: 1 / -1; margin-top: 1rem; margin-bottom: 0.5rem; font-weight: 600; color: #f472b6;">
565
+ 🕵️ Fact-Checks Trouvés (${factChecks.length})
566
+ </div>
567
+ `;
568
+
569
  factChecks.forEach(fc => {
570
+ detailsHTML += `
571
+ <div class="detail-card" style="grid-column: 1 / -1; border-color: rgba(244, 114, 182, 0.3);">
572
+ <div class="detail-label">🔍 ${fc.publisher || 'Source inconnue'}</div>
573
+ <div class="detail-value" style="font-size: 1rem; margin-bottom: 0.5rem;">"${fc.claim}"</div>
574
+ <div style="display: flex; justify-content: space-between; align-items: center;">
575
+ <span style="color: #f472b6; font-weight: 700;">Verdict: ${fc.rating}</span>
576
+ <a href="${fc.url}" target="_blank" style="color: #a855f7; text-decoration: none; font-size: 0.9rem;">Lire le rapport →</a>
577
+ </div>
578
+ </div>
579
+ `;
580
  });
581
  }
582
 
583
  detailsGrid.innerHTML = detailsHTML;
584
+
585
  results.classList.add('visible');
586
+
587
+ // Fetch and render graph with slight delay to ensure DOM is ready
588
+ requestAnimationFrame(() => {
589
+ renderD3Graph();
590
+ });
591
  }
592
 
593
  async function renderD3Graph() {
594
+ logDebug("Starting renderD3Graph...");
595
  const container = document.getElementById('cy');
596
+
597
+ // Check if D3 is loaded
598
+ if (typeof d3 === 'undefined') {
599
+ container.innerHTML = '<p class="error visible">Erreur: D3.js n\'a pas pu être chargé.</p>';
600
+ logDebug("ERROR: D3 undefined");
601
+ return;
602
+ }
603
 
604
  try {
605
+ container.innerHTML = '<div class="spinner"></div>'; // Loading state
606
+ logDebug("Fetching graph data...");
607
+
608
  const response = await fetch(`${API_URL}/api/ontology/graph`);
609
  const data = await response.json();
610
+
611
+ container.innerHTML = ''; // Clear loading
612
+ logDebug(`Data received. Nodes: ${data.nodes ? data.nodes.length : 0}, Links: ${data.links ? data.links.length : 0}`);
613
 
614
  if (!data.nodes || data.nodes.length === 0) {
615
+ container.innerHTML = '<p style="text-align:center; padding:2rem; color:#6b6b8a; width:100%; display:flex; justify-content:center; align-items:center; height:100%;">Ayçune donnée ontologique disponible.</p>';
616
  return;
617
  }
618
 
619
+ // Get dimensions
620
  const width = container.clientWidth || 800;
621
  const height = container.clientHeight || 500;
622
+ logDebug(`Container size: ${width}x${height}`);
623
 
624
  const svg = d3.select(container).append("svg")
625
+ .attr("width", "100%")
626
+ .attr("height", "100%")
627
  .attr("viewBox", [-width / 2, -height / 2, width, height])
628
+ .style("background-color", "rgba(0,0,0,0.2)"); // Visible background
629
+
630
+ // ADDED: Overlay for details
631
+ const overlay = document.createElement('div');
632
+ overlay.id = 'nodeDetails';
633
+ overlay.className = 'node-details-overlay';
634
+ overlay.innerHTML = `
635
+ <button class="close-btn" onclick="document.getElementById('nodeDetails').classList.remove('visible')">×</button>
636
+ <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>
637
+ <div id="detailBody" style="font-size:0.9rem; color:#ccc; line-height:1.5;"></div>
638
+ `;
639
+ container.appendChild(overlay);
640
+
641
+ logDebug("SVG created. Starting simulation...");
642
+
643
+ // Colors: 1=Purple(Report), 2=Gray(Unknown), 3=Green(Good), 4=Red(Bad)
644
+ const color = d3.scaleOrdinal()
645
+ .domain([1, 2, 3, 4])
646
+ .range(["#8b5cf6", "#94a3b8", "#22c55e", "#ef4444"]);
647
 
648
  const simulation = d3.forceSimulation(data.nodes)
649
  .force("link", d3.forceLink(data.links).id(d => d.id).distance(120))
650
  .force("charge", d3.forceManyBody().strength(-400))
651
  .force("center", d3.forceCenter(0, 0));
652
+
653
+ // ADDED: Container click to close overlay
654
+ svg.on("click", () => {
655
+ document.getElementById('nodeDetails').classList.remove('visible');
656
+ node.attr("stroke", "#fff").attr("stroke-width", 1.5);
657
+ });
658
 
659
+ // Arrow marker
660
+ svg.append("defs").selectAll("marker")
661
+ .data(["end"])
662
+ .join("marker")
663
+ .attr("id", "arrow")
664
+ .attr("viewBox", "0 -5 10 10")
665
+ .attr("refX", 22)
666
+ .attr("refY", 0)
667
+ .attr("markerWidth", 6)
668
+ .attr("markerHeight", 6)
669
+ .attr("orient", "auto")
670
+ .append("path")
671
+ .attr("fill", "#64748b")
672
+ .attr("d", "M0,-5L10,0L0,5");
673
+
674
+ const link = svg.append("g")
675
+ .selectAll("line")
676
+ .data(data.links)
677
+ .join("line")
678
+ .attr("stroke", "#475569")
679
+ .attr("stroke-opacity", 0.6)
680
+ .attr("stroke-width", 2)
681
+ .attr("marker-end", "url(#arrow)");
682
+
683
+ const node = svg.append("g")
684
+ .selectAll("circle")
685
+ .data(data.nodes)
686
+ .join("circle")
687
+ .attr("r", d => d.group === 1 ? 18 : 8)
688
+ .attr("fill", d => color(d.group))
689
+ .attr("stroke", "#fff")
690
+ .attr("stroke-width", 1.5)
691
+ .style("cursor", "pointer")
692
+ .call(drag(simulation))
693
+ .on("click", (event, d) => {
694
+ event.stopPropagation(); // Stop background click
695
+ showNodeDetails(d);
696
+
697
+ // Highlight selected
698
+ node.attr("stroke", "#fff").attr("stroke-width", 1.5);
699
+ d3.select(event.currentTarget).attr("stroke", "#f43f5e").attr("stroke-width", 3);
700
+ });
701
+
702
+ // Labels
703
+ const text = svg.append("g")
704
+ .selectAll("text")
705
+ .data(data.nodes)
706
+ .join("text")
707
  .text(d => d.name.length > 20 ? d.name.substring(0, 20) + "..." : d.name)
708
+ .attr("font-size", "11px")
709
+ .attr("fill", "#e0e0e0")
710
+ .attr("dx", 12)
711
+ .attr("dy", 4)
712
+ .style("pointer-events", "none")
713
+ .style("text-shadow", "0 1px 2px black");
714
+
715
+ // Tooltip
716
  node.append("title").text(d => `${d.name}\n(${d.type})`);
717
 
718
  simulation.on("tick", () => {
719
+ link
720
+ .attr("x1", d => d.source.x)
721
+ .attr("y1", d => d.source.y)
722
+ .attr("x2", d => d.target.x)
723
+ .attr("y2", d => d.target.y);
724
+
725
+ node
726
+ .attr("cx", d => d.x)
727
+ .attr("cy", d => d.y);
728
+
729
+ text
730
+ .attr("x", d => d.x)
731
+ .attr("y", d => d.y);
732
  });
733
 
734
+ // Zoom
735
+ svg.call(d3.zoom().scaleExtent([0.1, 4]).on("zoom", (e) => {
736
+ svg.selectAll('g').attr('transform', e.transform);
737
+ }));
738
 
739
+ logDebug("Graph rendered successfully.");
 
 
740
 
741
  } catch (err) {
742
+ console.error("D3 Graph error:", err);
743
+ const container = document.getElementById('cy');
744
+ if (container) container.innerHTML = `<p class="error visible">Erreur graphique: ${err.message}</p>`;
745
+ logDebug(`ERROR EXCEPTION: ${err.message}`);
746
  }
747
  }
748
 
749
+ function testD3() {
750
+ logDebug("Starting Static Test...");
751
+ const container = document.getElementById('cy');
752
+ container.innerHTML = '';
753
+
754
+ const width = container.clientWidth || 800;
755
+ const height = container.clientHeight || 500;
756
+
757
+ logDebug(`Container: ${width}x${height}`);
758
+
759
+ try {
760
+ const svg = d3.select(container).append("svg")
761
+ .attr("width", "100%")
762
+ .attr("height", "100%")
763
+ .attr("viewBox", [-width / 2, -height / 2, width, height])
764
+ .style("background-color", "#222");
765
+
766
+ svg.append("circle")
767
+ .attr("r", 50)
768
+ .attr("fill", "red")
769
+ .attr("cx", 0)
770
+ .attr("cy", 0);
771
+
772
+ svg.append("text")
773
+ .text("D3 WORKS")
774
+ .attr("fill", "white")
775
+ .attr("x", 0)
776
+ .attr("y", 5)
777
+ .attr("text-anchor", "middle");
778
+
779
+ logDebug("Static Test Complete. You should see a red circle.");
780
+ } catch (e) {
781
+ logDebug("Static Test ERROR: " + e.message);
782
+ alert("Static Test Failed: " + e.message);
783
+ }
784
+ }
785
+
786
+
787
+ // --- Helper Functions ---
788
+
789
+ function logDebug(msg) {
790
+ console.log(`[SysCRED Debug] ${msg}`);
791
+ }
792
+
793
+ function drag(simulation) {
794
+ function dragstarted(event) {
795
+ if (!event.active) simulation.alphaTarget(0.3).restart();
796
+ event.subject.fx = event.subject.x;
797
+ event.subject.fy = event.subject.y;
798
+ }
799
+
800
+ function dragged(event) {
801
+ event.subject.fx = event.x;
802
+ event.subject.fy = event.y;
803
+ }
804
+
805
+ function dragended(event) {
806
+ if (!event.active) simulation.alphaTarget(0);
807
+ event.subject.fx = null;
808
+ event.subject.fy = null;
809
+ }
810
+
811
+ return d3.drag()
812
+ .on("start", dragstarted)
813
+ .on("drag", dragged)
814
+ .on("end", dragended);
815
+ }
816
+
817
+ function showNodeDetails(d) {
818
+ const overlay = document.getElementById('nodeDetails');
819
+ const title = document.getElementById('detailTitle');
820
+ const body = document.getElementById('detailBody');
821
+
822
+ if(!overlay) return;
823
+
824
+ title.textContent = d.name;
825
+
826
+ let typeColor = "#94a3b8";
827
+ if(d.group === 1) typeColor = "#8b5cf6"; // Report
828
+ if(d.group === 3) typeColor = "#22c55e"; // Good
829
+ if(d.group === 4) typeColor = "#ef4444"; // Bad
830
+
831
+ body.innerHTML = `
832
+ <div style="margin-bottom:0.5rem">
833
+ <span style="background:${typeColor}; color:white; padding:2px 6px; border-radius:4px; font-size:0.75rem;">${d.type || 'Unknown Type'}</span>
834
+ </div>
835
+ <div><strong>URI:</strong> <br><span style="font-family:monospace; color:#a855f7; word-break:break-all;">${d.id}</span></div>
836
+ `;
837
+
838
+ overlay.classList.add('visible');
839
+ }
840
+
841
+ // Allow Enter key to trigger analysis
842
+ document.getElementById('urlInput').addEventListener('keypress', function (e) {
843
+ if (e.key === 'Enter') {
844
+ analyzeUrl();
845
+ }
846
+ });
847
  </script>
848
  </body>
849