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

Add backend toggle with auto-detect for HF Space environment

Browse files
Files changed (1) hide show
  1. syscred/static/index.html +156 -475
syscred/static/index.html CHANGED
@@ -17,7 +17,6 @@
17
  border: 1px solid rgba(255, 255, 255, 0.1);
18
  position: relative;
19
  display: block;
20
- /* Force display */
21
  }
22
 
23
  * {
@@ -119,25 +118,12 @@
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,29 +136,11 @@
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,23 +152,9 @@
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,17 +170,8 @@
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,21 +181,10 @@
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,11 +196,7 @@
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,63 +206,50 @@
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,12 +262,20 @@
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,35 +300,58 @@
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
- // Auto-detect API URL based on environment
397
- const API_URL = (() => {
 
 
 
 
398
  const hostname = window.location.hostname;
399
- if (hostname.includes('huggingface.co') || hostname.includes('hf.space')) {
400
- return '';
401
- } else if (hostname.includes('onrender.com')) {
 
 
 
 
402
  return '';
403
- } else {
404
- return 'http://localhost:5001';
405
  }
 
406
  })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
  async function analyzeUrl() {
409
  const urlInput = document.getElementById('urlInput');
@@ -413,13 +361,8 @@
413
  const btn = document.getElementById('analyzeBtn');
414
 
415
  const inputData = urlInput.value.trim();
 
416
 
417
- if (!inputData) {
418
- alert('Veuillez entrer une URL');
419
- return;
420
- }
421
-
422
- // Reset UI
423
  results.classList.remove('visible');
424
  error.classList.remove('visible');
425
  loading.classList.add('visible');
@@ -428,22 +371,12 @@
428
  try {
429
  const response = await fetch(`${API_URL}/api/verify`, {
430
  method: 'POST',
431
- headers: {
432
- 'Content-Type': 'application/json',
433
- },
434
- body: JSON.stringify({
435
- input_data: inputData,
436
- include_seo: true,
437
- include_pagerank: true
438
- })
439
  });
440
 
441
  const data = await response.json();
442
-
443
- if (!response.ok) {
444
- throw new Error(data.error || 'Erreur lors de l\'analyse');
445
- }
446
-
447
  displayResults(data);
448
 
449
  } catch (err) {
@@ -462,17 +395,14 @@
462
  const summary = document.getElementById('summary');
463
  const detailsGrid = document.getElementById('detailsGrid');
464
 
465
- // Score
466
  const score = data.scoreCredibilite || 0;
467
  scoreValue.textContent = score.toFixed(2);
468
 
469
- // Conditional Display: Hide Score Card if TEXT input, show if URL
470
  const isUrl = data.informationEntree && data.informationEntree.startsWith('http');
471
  const scoreCard = document.querySelector('.score-card');
472
 
473
  if (isUrl) {
474
  scoreCard.style.display = 'block';
475
- // Color based on score
476
  scoreValue.className = 'score-value';
477
  credibilityBadge.className = 'credibility-badge';
478
 
@@ -490,370 +420,121 @@
490
  credibilityBadge.textContent = '✗ Crédibilité Faible';
491
  }
492
  } else {
493
- // Hide score card for text queries as requested
494
  scoreCard.style.display = 'none';
495
  }
496
 
497
- // Summary
498
  summary.textContent = data.resumeAnalyse || 'Aucun résumé disponible';
499
 
500
- // Build details HTML
501
  let detailsHTML = '';
502
-
503
- // Source reputation from rule analysis
504
  const ruleResults = data.reglesAppliquees || {};
505
  const sourceAnalysis = ruleResults.source_analysis || {};
506
 
507
  if (sourceAnalysis.reputation) {
508
- const repColor = sourceAnalysis.reputation === 'High' ? '#22c55e' :
509
- sourceAnalysis.reputation === 'Low' ? '#ef4444' : '#eab308';
510
- detailsHTML += `
511
- <div class="detail-card">
512
- <div class="detail-label">🏛️ Réputation Source</div>
513
- <div class="detail-value" style="color: ${repColor}">${sourceAnalysis.reputation}</div>
514
- </div>
515
- `;
516
  }
517
 
518
  if (sourceAnalysis.domain_age_days) {
519
  const years = (sourceAnalysis.domain_age_days / 365).toFixed(1);
520
- detailsHTML += `
521
- <div class="detail-card">
522
- <div class="detail-label">📅 Âge du Domaine</div>
523
- <div class="detail-value">${years} ans</div>
524
- </div>
525
- `;
526
  }
527
 
528
- // NLP analysis
529
  const nlpAnalysis = data.analyseNLP || {};
530
-
531
  if (nlpAnalysis.sentiment) {
532
- detailsHTML += `
533
- <div class="detail-card">
534
- <div class="detail-label">💭 Sentiment</div>
535
- <div class="detail-value">${nlpAnalysis.sentiment.label} (${(nlpAnalysis.sentiment.score * 100).toFixed(0)}%)</div>
536
- </div>
537
- `;
538
  }
539
-
540
  if (nlpAnalysis.coherence_score !== null && nlpAnalysis.coherence_score !== undefined) {
541
- detailsHTML += `
542
- <div class="detail-card">
543
- <div class="detail-label">📊 Cohérence</div>
544
- <div class="detail-value">${(nlpAnalysis.coherence_score * 100).toFixed(0)}%</div>
545
- </div>
546
- `;
547
  }
548
 
549
- // Add PageRank if available
550
  if (data.pageRankEstimation && data.pageRankEstimation.estimatedPR) {
551
- detailsHTML += `
552
- <div class="detail-card">
553
- <div class="detail-label">📈 PageRank Estimé</div>
554
- <div class="detail-value">${data.pageRankEstimation.estimatedPR.toFixed(3)}</div>
555
- </div>
556
- `;
557
  }
558
-
559
- // Add SEO score if available
560
  if (data.seoAnalysis && data.seoAnalysis.seoScore) {
561
- detailsHTML += `
562
- <div class="detail-card">
563
- <div class="detail-label">🔍 Score SEO</div>
564
- <div class="detail-value">${data.seoAnalysis.seoScore}</div>
565
- </div>
566
- `;
567
  }
568
 
569
- // Fact checks
570
  const factChecks = ruleResults.fact_checking || [];
571
  if (factChecks.length > 0) {
572
- // Add a header for fact checks
573
- detailsHTML += `
574
- <div style="grid-column: 1 / -1; margin-top: 1rem; margin-bottom: 0.5rem; font-weight: 600; color: #f472b6;">
575
- 🕵️ Fact-Checks Trouvés (${factChecks.length})
576
- </div>
577
- `;
578
-
579
  factChecks.forEach(fc => {
580
- detailsHTML += `
581
- <div class="detail-card" style="grid-column: 1 / -1; border-color: rgba(244, 114, 182, 0.3);">
582
- <div class="detail-label">🔍 ${fc.publisher || 'Source inconnue'}</div>
583
- <div class="detail-value" style="font-size: 1rem; margin-bottom: 0.5rem;">"${fc.claim}"</div>
584
- <div style="display: flex; justify-content: space-between; align-items: center;">
585
- <span style="color: #f472b6; font-weight: 700;">Verdict: ${fc.rating}</span>
586
- <a href="${fc.url}" target="_blank" style="color: #a855f7; text-decoration: none; font-size: 0.9rem;">Lire le rapport →</a>
587
- </div>
588
- </div>
589
- `;
590
  });
591
  }
592
 
593
  detailsGrid.innerHTML = detailsHTML;
594
-
595
  results.classList.add('visible');
596
-
597
- // Fetch and render graph with slight delay to ensure DOM is ready
598
- requestAnimationFrame(() => {
599
- renderD3Graph();
600
- });
601
  }
602
 
603
  async function renderD3Graph() {
604
- logDebug("Starting renderD3Graph...");
605
  const container = document.getElementById('cy');
606
-
607
- // Check if D3 is loaded
608
- if (typeof d3 === 'undefined') {
609
- container.innerHTML = '<p class="error visible">Erreur: D3.js n\'a pas pu être chargé.</p>';
610
- logDebug("ERROR: D3 undefined");
611
- return;
612
- }
613
 
614
  try {
615
- container.innerHTML = '<div class="spinner"></div>'; // Loading state
616
- logDebug("Fetching graph data...");
617
-
618
  const response = await fetch(`${API_URL}/api/ontology/graph`);
619
  const data = await response.json();
620
-
621
- container.innerHTML = ''; // Clear loading
622
- logDebug(`Data received. Nodes: ${data.nodes ? data.nodes.length : 0}, Links: ${data.links ? data.links.length : 0}`);
623
 
624
  if (!data.nodes || data.nodes.length === 0) {
625
- 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>';
626
  return;
627
  }
628
 
629
- // Get dimensions
630
  const width = container.clientWidth || 800;
631
  const height = container.clientHeight || 500;
632
- logDebug(`Container size: ${width}x${height}`);
633
 
634
  const svg = d3.select(container).append("svg")
635
- .attr("width", "100%")
636
- .attr("height", "100%")
637
  .attr("viewBox", [-width / 2, -height / 2, width, height])
638
- .style("background-color", "rgba(0,0,0,0.2)"); // Visible background
639
-
640
- // ADDED: Overlay for details
641
- const overlay = document.createElement('div');
642
- overlay.id = 'nodeDetails';
643
- overlay.className = 'node-details-overlay';
644
- overlay.innerHTML = `
645
- <button class="close-btn" onclick="document.getElementById('nodeDetails').classList.remove('visible')">×</button>
646
- <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>
647
- <div id="detailBody" style="font-size:0.9rem; color:#ccc; line-height:1.5;"></div>
648
- `;
649
- container.appendChild(overlay);
650
-
651
- logDebug("SVG created. Starting simulation...");
652
-
653
- // Colors: 1=Purple(Report), 2=Gray(Unknown), 3=Green(Good), 4=Red(Bad)
654
- const color = d3.scaleOrdinal()
655
- .domain([1, 2, 3, 4])
656
- .range(["#8b5cf6", "#94a3b8", "#22c55e", "#ef4444"]);
657
 
658
  const simulation = d3.forceSimulation(data.nodes)
659
  .force("link", d3.forceLink(data.links).id(d => d.id).distance(120))
660
  .force("charge", d3.forceManyBody().strength(-400))
661
  .force("center", d3.forceCenter(0, 0));
662
-
663
- // ADDED: Container click to close overlay
664
- svg.on("click", () => {
665
- document.getElementById('nodeDetails').classList.remove('visible');
666
- node.attr("stroke", "#fff").attr("stroke-width", 1.5);
667
- });
668
 
669
- // Arrow marker
670
- svg.append("defs").selectAll("marker")
671
- .data(["end"])
672
- .join("marker")
673
- .attr("id", "arrow")
674
- .attr("viewBox", "0 -5 10 10")
675
- .attr("refX", 22)
676
- .attr("refY", 0)
677
- .attr("markerWidth", 6)
678
- .attr("markerHeight", 6)
679
- .attr("orient", "auto")
680
- .append("path")
681
- .attr("fill", "#64748b")
682
- .attr("d", "M0,-5L10,0L0,5");
683
-
684
- const link = svg.append("g")
685
- .selectAll("line")
686
- .data(data.links)
687
- .join("line")
688
- .attr("stroke", "#475569")
689
- .attr("stroke-opacity", 0.6)
690
- .attr("stroke-width", 2)
691
- .attr("marker-end", "url(#arrow)");
692
-
693
- const node = svg.append("g")
694
- .selectAll("circle")
695
- .data(data.nodes)
696
- .join("circle")
697
- .attr("r", d => d.group === 1 ? 18 : 8)
698
- .attr("fill", d => color(d.group))
699
- .attr("stroke", "#fff")
700
- .attr("stroke-width", 1.5)
701
- .style("cursor", "pointer")
702
- .call(drag(simulation))
703
- .on("click", (event, d) => {
704
- event.stopPropagation(); // Stop background click
705
- showNodeDetails(d);
706
-
707
- // Highlight selected
708
- node.attr("stroke", "#fff").attr("stroke-width", 1.5);
709
- d3.select(event.currentTarget).attr("stroke", "#f43f5e").attr("stroke-width", 3);
710
- });
711
-
712
- // Labels
713
- const text = svg.append("g")
714
- .selectAll("text")
715
- .data(data.nodes)
716
- .join("text")
717
  .text(d => d.name.length > 20 ? d.name.substring(0, 20) + "..." : d.name)
718
- .attr("font-size", "11px")
719
- .attr("fill", "#e0e0e0")
720
- .attr("dx", 12)
721
- .attr("dy", 4)
722
- .style("pointer-events", "none")
723
- .style("text-shadow", "0 1px 2px black");
724
-
725
- // Tooltip
726
  node.append("title").text(d => `${d.name}\n(${d.type})`);
727
 
728
  simulation.on("tick", () => {
729
- link
730
- .attr("x1", d => d.source.x)
731
- .attr("y1", d => d.source.y)
732
- .attr("x2", d => d.target.x)
733
- .attr("y2", d => d.target.y);
734
-
735
- node
736
- .attr("cx", d => d.x)
737
- .attr("cy", d => d.y);
738
-
739
- text
740
- .attr("x", d => d.x)
741
- .attr("y", d => d.y);
742
  });
743
 
744
- // Zoom
745
- svg.call(d3.zoom().scaleExtent([0.1, 4]).on("zoom", (e) => {
746
- svg.selectAll('g').attr('transform', e.transform);
747
- }));
748
 
749
- logDebug("Graph rendered successfully.");
 
 
750
 
751
  } catch (err) {
752
- console.error("D3 Graph error:", err);
753
- const container = document.getElementById('cy');
754
- if (container) container.innerHTML = `<p class="error visible">Erreur graphique: ${err.message}</p>`;
755
- logDebug(`ERROR EXCEPTION: ${err.message}`);
756
- }
757
- }
758
-
759
- function testD3() {
760
- logDebug("Starting Static Test...");
761
- const container = document.getElementById('cy');
762
- container.innerHTML = '';
763
-
764
- const width = container.clientWidth || 800;
765
- const height = container.clientHeight || 500;
766
-
767
- logDebug(`Container: ${width}x${height}`);
768
-
769
- try {
770
- const svg = d3.select(container).append("svg")
771
- .attr("width", "100%")
772
- .attr("height", "100%")
773
- .attr("viewBox", [-width / 2, -height / 2, width, height])
774
- .style("background-color", "#222");
775
-
776
- svg.append("circle")
777
- .attr("r", 50)
778
- .attr("fill", "red")
779
- .attr("cx", 0)
780
- .attr("cy", 0);
781
-
782
- svg.append("text")
783
- .text("D3 WORKS")
784
- .attr("fill", "white")
785
- .attr("x", 0)
786
- .attr("y", 5)
787
- .attr("text-anchor", "middle");
788
-
789
- logDebug("Static Test Complete. You should see a red circle.");
790
- } catch (e) {
791
- logDebug("Static Test ERROR: " + e.message);
792
- alert("Static Test Failed: " + e.message);
793
- }
794
- }
795
-
796
-
797
- // --- Helper Functions ---
798
-
799
- function logDebug(msg) {
800
- console.log(`[SysCRED Debug] ${msg}`);
801
- }
802
-
803
- function drag(simulation) {
804
- function dragstarted(event) {
805
- if (!event.active) simulation.alphaTarget(0.3).restart();
806
- event.subject.fx = event.subject.x;
807
- event.subject.fy = event.subject.y;
808
- }
809
-
810
- function dragged(event) {
811
- event.subject.fx = event.x;
812
- event.subject.fy = event.y;
813
  }
814
-
815
- function dragended(event) {
816
- if (!event.active) simulation.alphaTarget(0);
817
- event.subject.fx = null;
818
- event.subject.fy = null;
819
- }
820
-
821
- return d3.drag()
822
- .on("start", dragstarted)
823
- .on("drag", dragged)
824
- .on("end", dragended);
825
- }
826
-
827
- function showNodeDetails(d) {
828
- const overlay = document.getElementById('nodeDetails');
829
- const title = document.getElementById('detailTitle');
830
- const body = document.getElementById('detailBody');
831
-
832
- if(!overlay) return;
833
-
834
- title.textContent = d.name;
835
-
836
- let typeColor = "#94a3b8";
837
- if(d.group === 1) typeColor = "#8b5cf6"; // Report
838
- if(d.group === 3) typeColor = "#22c55e"; // Good
839
- if(d.group === 4) typeColor = "#ef4444"; // Bad
840
-
841
- body.innerHTML = `
842
- <div style="margin-bottom:0.5rem">
843
- <span style="background:${typeColor}; color:white; padding:2px 6px; border-radius:4px; font-size:0.75rem;">${d.type || 'Unknown Type'}</span>
844
- </div>
845
- <div><strong>URI:</strong> <br><span style="font-family:monospace; color:#a855f7; word-break:break-all;">${d.id}</span></div>
846
- `;
847
-
848
- overlay.classList.add('visible');
849
  }
850
 
851
- // Allow Enter key to trigger analysis
852
- document.getElementById('urlInput').addEventListener('keypress', function (e) {
853
- if (e.key === 'Enter') {
854
- analyzeUrl();
855
- }
856
- });
857
  </script>
858
  </body>
859
 
 
17
  border: 1px solid rgba(255, 255, 255, 0.1);
18
  position: relative;
19
  display: block;
 
20
  }
21
 
22
  * {
 
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
  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
  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
  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
  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
  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
  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
 
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
  <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
  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
  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
  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
  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