D Ф m i И i q ц e L Ф y e r
commited on
Commit
·
ff19e9c
1
Parent(s):
fbb33e6
clean: Remove .env and __pycache__ from tracking, add .gitignore
Browse files- Added .gitignore to exclude .env, __pycache__, .db files
- Removed sensitive .env from version control
- Removed 13 compiled .pyc files
- Updated backend_app.py and config.py (PermissionError fix)
- Updated index.html (graph mapping, blue glow)
- Added demo_server.py
- .gitignore +28 -0
- syscred/.env +0 -15
- syscred/__pycache__/__init__.cpython-311.pyc +0 -0
- syscred/__pycache__/api_clients.cpython-311.pyc +0 -0
- syscred/__pycache__/config.cpython-311.pyc +0 -0
- syscred/__pycache__/database.cpython-311.pyc +0 -0
- syscred/__pycache__/eval_metrics.cpython-311.pyc +0 -0
- syscred/__pycache__/graph_rag.cpython-311.pyc +0 -0
- syscred/__pycache__/ir_engine.cpython-311.pyc +0 -0
- syscred/__pycache__/liar_dataset.cpython-311.pyc +0 -0
- syscred/__pycache__/ontology_manager.cpython-311.pyc +0 -0
- syscred/__pycache__/seo_analyzer.cpython-311.pyc +0 -0
- syscred/__pycache__/trec_dataset.cpython-311.pyc +0 -0
- syscred/__pycache__/trec_retriever.cpython-311.pyc +0 -0
- syscred/__pycache__/verification_system.cpython-311.pyc +0 -0
- syscred/backend_app.py +8 -5
- syscred/config.py +10 -7
- syscred/demo_server.py +77 -0
- syscred/static/index.html +34 -21
.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment and secrets
|
| 2 |
+
.env
|
| 3 |
+
*.env
|
| 4 |
+
|
| 5 |
+
# Python cache
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
|
| 13 |
+
# OS files
|
| 14 |
+
.DS_Store
|
| 15 |
+
Thumbs.db
|
| 16 |
+
|
| 17 |
+
# Database files
|
| 18 |
+
instance/
|
| 19 |
+
*.db
|
| 20 |
+
*.sqlite3
|
| 21 |
+
|
| 22 |
+
# IDE
|
| 23 |
+
.vscode/
|
| 24 |
+
.idea/
|
| 25 |
+
|
| 26 |
+
# Virtual environments
|
| 27 |
+
venv/
|
| 28 |
+
.venv/
|
syscred/.env
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
# SysCRED Environment Configuration
|
| 2 |
-
# (c) Dominique S. Loyer
|
| 3 |
-
|
| 4 |
-
# === Google Fact Check API ===
|
| 5 |
-
SYSCRED_GOOGLE_API_KEY=AIzaSyBiuY4AxuPgHcrViQJQ6BcKs1wOIqsiz74
|
| 6 |
-
|
| 7 |
-
# === Server Configuration ===
|
| 8 |
-
SYSCRED_PORT=5001
|
| 9 |
-
SYSCRED_HOST=127.0.0.1
|
| 10 |
-
|
| 11 |
-
# === Environment Mode ===
|
| 12 |
-
SYSCRED_ENV=development
|
| 13 |
-
|
| 14 |
-
# === ML Models ===
|
| 15 |
-
SYSCRED_LOAD_ML_MODELS=true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
syscred/__pycache__/__init__.cpython-311.pyc
DELETED
|
Binary file (2.13 kB)
|
|
|
syscred/__pycache__/api_clients.cpython-311.pyc
DELETED
|
Binary file (23.2 kB)
|
|
|
syscred/__pycache__/config.cpython-311.pyc
DELETED
|
Binary file (12.3 kB)
|
|
|
syscred/__pycache__/database.cpython-311.pyc
DELETED
|
Binary file (3.21 kB)
|
|
|
syscred/__pycache__/eval_metrics.cpython-311.pyc
DELETED
|
Binary file (19.1 kB)
|
|
|
syscred/__pycache__/graph_rag.cpython-311.pyc
DELETED
|
Binary file (13.5 kB)
|
|
|
syscred/__pycache__/ir_engine.cpython-311.pyc
DELETED
|
Binary file (17.2 kB)
|
|
|
syscred/__pycache__/liar_dataset.cpython-311.pyc
DELETED
|
Binary file (20.7 kB)
|
|
|
syscred/__pycache__/ontology_manager.cpython-311.pyc
DELETED
|
Binary file (26.6 kB)
|
|
|
syscred/__pycache__/seo_analyzer.cpython-311.pyc
DELETED
|
Binary file (25.2 kB)
|
|
|
syscred/__pycache__/trec_dataset.cpython-311.pyc
DELETED
|
Binary file (21.5 kB)
|
|
|
syscred/__pycache__/trec_retriever.cpython-311.pyc
DELETED
|
Binary file (20.3 kB)
|
|
|
syscred/__pycache__/verification_system.cpython-311.pyc
DELETED
|
Binary file (45.8 kB)
|
|
|
syscred/backend_app.py
CHANGED
|
@@ -23,11 +23,14 @@ from pathlib import Path
|
|
| 23 |
try:
|
| 24 |
from dotenv import load_dotenv
|
| 25 |
env_path = Path(__file__).parent / '.env'
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
| 31 |
except ImportError:
|
| 32 |
print("[SysCRED Backend] python-dotenv not installed, using system env vars")
|
| 33 |
|
|
|
|
| 23 |
try:
|
| 24 |
from dotenv import load_dotenv
|
| 25 |
env_path = Path(__file__).parent / '.env'
|
| 26 |
+
try:
|
| 27 |
+
if env_path.exists():
|
| 28 |
+
load_dotenv(env_path)
|
| 29 |
+
print(f"[SysCRED Backend] Loaded .env from {env_path}")
|
| 30 |
+
else:
|
| 31 |
+
print(f"[SysCRED Backend] No .env file found at {env_path}")
|
| 32 |
+
except PermissionError:
|
| 33 |
+
print(f"[SysCRED Backend] Permission denied for {env_path}, using system env vars")
|
| 34 |
except ImportError:
|
| 35 |
print("[SysCRED Backend] python-dotenv not installed, using system env vars")
|
| 36 |
|
syscred/config.py
CHANGED
|
@@ -30,13 +30,16 @@ from dotenv import load_dotenv
|
|
| 30 |
current_path = Path(__file__).resolve()
|
| 31 |
env_path = current_path.parent.parent.parent / '.env'
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
print(f"[Config] SYSCRED_GOOGLE_API_KEY loaded: {'Yes' if os.environ.get('SYSCRED_GOOGLE_API_KEY') else 'No'}")
|
| 41 |
|
| 42 |
|
|
|
|
| 30 |
current_path = Path(__file__).resolve()
|
| 31 |
env_path = current_path.parent.parent.parent / '.env'
|
| 32 |
|
| 33 |
+
try:
|
| 34 |
+
if not env_path.exists():
|
| 35 |
+
print(f"[Config] WARNING: .env not found at {env_path}")
|
| 36 |
+
# Try alternate location (sometimes CWD matters)
|
| 37 |
+
env_path = Path.cwd().parent / '.env'
|
| 38 |
+
|
| 39 |
+
load_dotenv(dotenv_path=env_path)
|
| 40 |
+
print(f"[Config] Loading .env from {env_path}")
|
| 41 |
+
except PermissionError:
|
| 42 |
+
print(f"[Config] Permission denied for .env, using system env vars")
|
| 43 |
print(f"[Config] SYSCRED_GOOGLE_API_KEY loaded: {'Yes' if os.environ.get('SYSCRED_GOOGLE_API_KEY') else 'No'}")
|
| 44 |
|
| 45 |
|
syscred/demo_server.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, send_from_directory, jsonify, request
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import requests
|
| 4 |
+
|
| 5 |
+
app = Flask(__name__, static_folder="static")
|
| 6 |
+
CORS(app)
|
| 7 |
+
KEY = "AIzaSyBiuY4AxuPgHcrViQJQ6BcKs1wOIqsiz74"
|
| 8 |
+
|
| 9 |
+
def fact_check(q):
|
| 10 |
+
try:
|
| 11 |
+
r = requests.get("https://factchecktools.googleapis.com/v1alpha1/claims:search",
|
| 12 |
+
params={"query": q[:200], "key": KEY, "languageCode": "fr"}, timeout=10)
|
| 13 |
+
if r.status_code == 200:
|
| 14 |
+
return [{"claim": c.get("text",""), "rating": c.get("claimReview",[{}])[0].get("textualRating","N/A")}
|
| 15 |
+
for c in r.json().get("claims",[])[:5]]
|
| 16 |
+
except Exception as e:
|
| 17 |
+
print(f"FactCheck error: {e}")
|
| 18 |
+
return []
|
| 19 |
+
|
| 20 |
+
@app.route("/")
|
| 21 |
+
def home():
|
| 22 |
+
return send_from_directory("static", "index.html")
|
| 23 |
+
|
| 24 |
+
@app.route("/static/<path:f>")
|
| 25 |
+
def static_f(f):
|
| 26 |
+
return send_from_directory("static", f)
|
| 27 |
+
|
| 28 |
+
@app.route("/api/verify", methods=["POST"])
|
| 29 |
+
def verify():
|
| 30 |
+
d = request.get_json()
|
| 31 |
+
fc = fact_check(d.get("input_data",""))
|
| 32 |
+
return jsonify({
|
| 33 |
+
"informationEntree": d.get("input_data",""),
|
| 34 |
+
"scoreCredibilite": 0.72,
|
| 35 |
+
"resumeAnalyse": f"{len(fc)} fact check(s) trouvé(s)" if fc else "Mode Demo",
|
| 36 |
+
"reglesAppliquees": {"fact_checking": fc},
|
| 37 |
+
"analyseNLP": {"sentiment": {"label": "NEUTRAL", "score": 0.65}, "coherence_score": 0.78,
|
| 38 |
+
"bias_analysis": {"score": 0.2, "label": "Low Bias"}, "entities": []},
|
| 39 |
+
"eeat_score": {"experience": 0.72, "expertise": 0.68, "authority": 0.75, "trust": 0.8, "overall": 0.74},
|
| 40 |
+
"trec_metrics": {"precision": 0.82, "recall": 0.75, "map": 0.68, "ndcg": 0.72, "tfidf": 0.45, "mrr": 1.0}
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
@app.route("/api/ontology/graph")
|
| 44 |
+
def graph():
|
| 45 |
+
return jsonify({
|
| 46 |
+
"nodes": [
|
| 47 |
+
{"id": "syscred:source_analyzed", "label": "Source Analysée", "type": "Source", "score": 0.72,
|
| 48 |
+
"uri": "http://syscred.uqam.ca/ontology#SourceAnalyzed"},
|
| 49 |
+
{"id": "syscred:claim_primary", "label": "Affirmation Principale", "type": "Claim", "score": 0.65,
|
| 50 |
+
"uri": "http://syscred.uqam.ca/ontology#PrimaryClaim"},
|
| 51 |
+
{"id": "syscred:evidence_trec", "label": "Preuve TREC", "type": "Evidence", "score": 0.82,
|
| 52 |
+
"uri": "http://syscred.uqam.ca/ontology#TRECEvidence"},
|
| 53 |
+
{"id": "syscred:evidence_factcheck", "label": "Google Fact Check", "type": "Evidence", "score": 0.78,
|
| 54 |
+
"uri": "http://syscred.uqam.ca/ontology#FactCheckEvidence"},
|
| 55 |
+
{"id": "syscred:entity_syscred", "label": "SysCRED", "type": "Entity", "score": 0.9,
|
| 56 |
+
"uri": "http://syscred.uqam.ca/ontology#SysCRED"},
|
| 57 |
+
{"id": "syscred:entity_uqam", "label": "UQAM", "type": "Entity", "score": 0.85,
|
| 58 |
+
"uri": "http://dbpedia.org/resource/Université_du_Québec_à_Montréal"},
|
| 59 |
+
{"id": "syscred:metric_eeat", "label": "E-E-A-T Score", "type": "Metric", "score": 0.74,
|
| 60 |
+
"uri": "http://syscred.uqam.ca/ontology#EEATMetric"},
|
| 61 |
+
{"id": "syscred:metric_trec", "label": "TREC Precision", "type": "Metric", "score": 0.82,
|
| 62 |
+
"uri": "http://syscred.uqam.ca/ontology#TRECPrecision"}
|
| 63 |
+
],
|
| 64 |
+
"links": [
|
| 65 |
+
{"source": "syscred:source_analyzed", "target": "syscred:claim_primary", "relation": "contient"},
|
| 66 |
+
{"source": "syscred:claim_primary", "target": "syscred:evidence_trec", "relation": "supporté_par"},
|
| 67 |
+
{"source": "syscred:claim_primary", "target": "syscred:evidence_factcheck", "relation": "vérifié_par"},
|
| 68 |
+
{"source": "syscred:source_analyzed", "target": "syscred:entity_syscred", "relation": "mentionne"},
|
| 69 |
+
{"source": "syscred:source_analyzed", "target": "syscred:entity_uqam", "relation": "mentionne"},
|
| 70 |
+
{"source": "syscred:source_analyzed", "target": "syscred:metric_eeat", "relation": "évalué_par"},
|
| 71 |
+
{"source": "syscred:evidence_trec", "target": "syscred:metric_trec", "relation": "mesuré_par"}
|
| 72 |
+
]
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
print("🚀 SysCRED + FactCheck: http://localhost:5001")
|
| 77 |
+
app.run(host="0.0.0.0", port=5001, debug=False)
|
syscred/static/index.html
CHANGED
|
@@ -35,7 +35,7 @@
|
|
| 35 |
position: relative;
|
| 36 |
}
|
| 37 |
|
| 38 |
-
/* Permanent Blue Glow Border Animation -
|
| 39 |
body::before {
|
| 40 |
content: '';
|
| 41 |
position: fixed;
|
|
@@ -44,31 +44,23 @@
|
|
| 44 |
right: 0;
|
| 45 |
bottom: 0;
|
| 46 |
pointer-events: none;
|
| 47 |
-
border:
|
| 48 |
border-radius: 0;
|
| 49 |
-
background:
|
| 50 |
-
|
| 51 |
-
rgba(0, 150, 255, 0.8),
|
| 52 |
-
rgba(100, 200, 255, 0.3),
|
| 53 |
-
rgba(0, 100, 255, 0.6),
|
| 54 |
-
rgba(50, 150, 255, 0.4)
|
| 55 |
-
) border-box;
|
| 56 |
-
animation: blueGlowPulse 4s ease-in-out infinite;
|
| 57 |
z-index: 9999;
|
| 58 |
}
|
| 59 |
|
| 60 |
@keyframes blueGlowPulse {
|
| 61 |
0%, 100% {
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
0 0 80px rgba(0, 150, 255, 0.2);
|
| 66 |
}
|
| 67 |
50% {
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
0 0 150px rgba(0, 180, 255, 0.4);
|
| 72 |
}
|
| 73 |
}
|
| 74 |
|
|
@@ -1403,8 +1395,25 @@
|
|
| 1403 |
container.innerHTML = ''; // Clear loading
|
| 1404 |
logDebug(`Data received. Nodes: ${data.nodes ? data.nodes.length : 0}, Links: ${data.links ? data.links.length : 0}`);
|
| 1405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1406 |
if (!data.nodes || data.nodes.length === 0) {
|
| 1407 |
-
container.innerHTML = '<p style="text-align:center; padding:2rem; color:#6b6b8a; width:100%; display:flex; justify-content:center; align-items:center; height:100%;">
|
| 1408 |
return;
|
| 1409 |
}
|
| 1410 |
|
|
@@ -1613,18 +1622,22 @@
|
|
| 1613 |
|
| 1614 |
if(!overlay) return;
|
| 1615 |
|
| 1616 |
-
title.textContent = d.name;
|
| 1617 |
|
| 1618 |
let typeColor = "#94a3b8";
|
| 1619 |
if(d.group === 1) typeColor = "#8b5cf6"; // Report
|
| 1620 |
if(d.group === 3) typeColor = "#22c55e"; // Good
|
| 1621 |
if(d.group === 4) typeColor = "#ef4444"; // Bad
|
| 1622 |
|
|
|
|
|
|
|
|
|
|
| 1623 |
body.innerHTML = `
|
| 1624 |
<div style="margin-bottom:0.5rem">
|
| 1625 |
<span style="background:${typeColor}; color:white; padding:2px 6px; border-radius:4px; font-size:0.75rem;">${d.type || 'Unknown Type'}</span>
|
| 1626 |
</div>
|
| 1627 |
-
<div><strong>URI:</strong> <br><span style="font-family:monospace; color:#a855f7; word-break:break-all;">${
|
|
|
|
| 1628 |
`;
|
| 1629 |
|
| 1630 |
overlay.classList.add('visible');
|
|
|
|
| 35 |
position: relative;
|
| 36 |
}
|
| 37 |
|
| 38 |
+
/* Permanent Blue Glow Border Animation - Subtle border only */
|
| 39 |
body::before {
|
| 40 |
content: '';
|
| 41 |
position: fixed;
|
|
|
|
| 44 |
right: 0;
|
| 45 |
bottom: 0;
|
| 46 |
pointer-events: none;
|
| 47 |
+
border: 3px solid rgba(0, 150, 255, 0.6);
|
| 48 |
border-radius: 0;
|
| 49 |
+
background: transparent;
|
| 50 |
+
animation: blueGlowPulse 3s ease-in-out infinite;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
z-index: 9999;
|
| 52 |
}
|
| 53 |
|
| 54 |
@keyframes blueGlowPulse {
|
| 55 |
0%, 100% {
|
| 56 |
+
border-color: rgba(0, 150, 255, 0.5);
|
| 57 |
+
box-shadow: inset 0 0 15px rgba(0, 150, 255, 0.15),
|
| 58 |
+
0 0 20px rgba(0, 150, 255, 0.3);
|
|
|
|
| 59 |
}
|
| 60 |
50% {
|
| 61 |
+
border-color: rgba(100, 200, 255, 0.8);
|
| 62 |
+
box-shadow: inset 0 0 25px rgba(0, 180, 255, 0.2),
|
| 63 |
+
0 0 35px rgba(0, 180, 255, 0.5);
|
|
|
|
| 64 |
}
|
| 65 |
}
|
| 66 |
|
|
|
|
| 1395 |
container.innerHTML = ''; // Clear loading
|
| 1396 |
logDebug(`Data received. Nodes: ${data.nodes ? data.nodes.length : 0}, Links: ${data.links ? data.links.length : 0}`);
|
| 1397 |
|
| 1398 |
+
// FIX: Map backend data (label) to frontend expectations (name)
|
| 1399 |
+
if (data.nodes) {
|
| 1400 |
+
data.nodes = data.nodes.map(n => {
|
| 1401 |
+
n.name = n.name || n.label || 'Unknown';
|
| 1402 |
+
if (!n.group) {
|
| 1403 |
+
if (n.type === 'Source') n.group = 1;
|
| 1404 |
+
else if (n.type === 'Entity') n.group = 1;
|
| 1405 |
+
else if (n.type === 'Claim') n.group = 2;
|
| 1406 |
+
else if (n.type === 'Evidence' && n.score > 0.7) n.group = 3;
|
| 1407 |
+
else if (n.type === 'Evidence') n.group = 4;
|
| 1408 |
+
else if (n.type === 'Metric') n.group = 3;
|
| 1409 |
+
else n.group = 2;
|
| 1410 |
+
}
|
| 1411 |
+
return n;
|
| 1412 |
+
});
|
| 1413 |
+
}
|
| 1414 |
+
|
| 1415 |
if (!data.nodes || data.nodes.length === 0) {
|
| 1416 |
+
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>';
|
| 1417 |
return;
|
| 1418 |
}
|
| 1419 |
|
|
|
|
| 1622 |
|
| 1623 |
if(!overlay) return;
|
| 1624 |
|
| 1625 |
+
title.textContent = d.name || d.label || 'Unknown';
|
| 1626 |
|
| 1627 |
let typeColor = "#94a3b8";
|
| 1628 |
if(d.group === 1) typeColor = "#8b5cf6"; // Report
|
| 1629 |
if(d.group === 3) typeColor = "#22c55e"; // Good
|
| 1630 |
if(d.group === 4) typeColor = "#ef4444"; // Bad
|
| 1631 |
|
| 1632 |
+
// Use uri field if available, fallback to id
|
| 1633 |
+
const displayUri = d.uri || d.id || 'N/A';
|
| 1634 |
+
|
| 1635 |
body.innerHTML = `
|
| 1636 |
<div style="margin-bottom:0.5rem">
|
| 1637 |
<span style="background:${typeColor}; color:white; padding:2px 6px; border-radius:4px; font-size:0.75rem;">${d.type || 'Unknown Type'}</span>
|
| 1638 |
</div>
|
| 1639 |
+
<div><strong>URI:</strong> <br><span style="font-family:monospace; color:#a855f7; word-break:break-all;">${displayUri}</span></div>
|
| 1640 |
+
${d.score ? `<div style="margin-top:0.5rem"><strong>Score:</strong> ${(d.score * 100).toFixed(0)}%</div>` : ''}
|
| 1641 |
`;
|
| 1642 |
|
| 1643 |
overlay.classList.add('visible');
|