Spaces:
Runtime error
Runtime error
Karim Krklec commited on
Commit ·
dcce181
0
Parent(s):
'first'
Browse files- .gitattributes +1 -0
- __pycache__/classifier.cpython-313.pyc +0 -0
- __pycache__/feature_extraction.cpython-313.pyc +0 -0
- __pycache__/language_config.cpython-313.pyc +0 -0
- app.py +252 -0
- classifier.py +706 -0
- data/dataset.csv +0 -0
- download_dataset.py +261 -0
- feature_extraction.py +625 -0
- frontend/.gitignore +24 -0
- frontend/README.md +16 -0
- frontend/eslint.config.js +21 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +1677 -0
- frontend/package.json +19 -0
- frontend/public/favicon.svg +1 -0
- frontend/public/icons.svg +24 -0
- frontend/src/App.css +184 -0
- frontend/src/App.jsx +62 -0
- frontend/src/api/client.js +31 -0
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/assets/vite.svg +1 -0
- frontend/src/components/CodeEditor.jsx +105 -0
- frontend/src/components/DetailSidebar.jsx +65 -0
- frontend/src/components/Dropzone.jsx +46 -0
- frontend/src/components/ExplanationPanel.jsx +130 -0
- frontend/src/components/FeatureCard.jsx +50 -0
- frontend/src/components/Heatmap.jsx +200 -0
- frontend/src/components/Navbar.jsx +68 -0
- frontend/src/components/ResultGauge.jsx +60 -0
- frontend/src/components/ResultsTable.jsx +113 -0
- frontend/src/components/primitives.jsx +188 -0
- frontend/src/index.css +111 -0
- frontend/src/main.jsx +10 -0
- frontend/src/screens/AnalyzeScreen.jsx +202 -0
- frontend/src/screens/BatchScreen.jsx +138 -0
- frontend/src/screens/DetailScreen.jsx +105 -0
- frontend/src/screens/SimilarityScreen.jsx +125 -0
- frontend/src/styles/tokens.css +53 -0
- frontend/vite.config.js +15 -0
- language_config.py +281 -0
- main.py +239 -0
- model/classifier.pkl +3 -0
- model/feature_names.pkl +0 -0
- model/scaler.pkl +0 -0
- primjeri/1.py +10 -0
- primjeri/11.py +9 -0
- primjeri/ai.py +116 -0
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
model/classifier.pkl filter=lfs diff=lfs merge=lfs -text
|
__pycache__/classifier.cpython-313.pyc
ADDED
|
Binary file (26.4 kB). View file
|
|
|
__pycache__/feature_extraction.cpython-313.pyc
ADDED
|
Binary file (25.9 kB). View file
|
|
|
__pycache__/language_config.cpython-313.pyc
ADDED
|
Binary file (9.16 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import difflib
|
| 4 |
+
import warnings
|
| 5 |
+
warnings.filterwarnings("ignore")
|
| 6 |
+
|
| 7 |
+
from flask import Flask, request, jsonify
|
| 8 |
+
from flask_cors import CORS
|
| 9 |
+
|
| 10 |
+
from classifier import predict, ucitaj_model
|
| 11 |
+
from feature_extraction import extract_all_features
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 15 |
+
# INICIJALIZACIJA
|
| 16 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 17 |
+
|
| 18 |
+
app = Flask(__name__)
|
| 19 |
+
|
| 20 |
+
# CORS dopušta React frontendu (localhost:3000) da komunicira s ovim serverom
|
| 21 |
+
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
| 22 |
+
|
| 23 |
+
# Učitaj model jednom pri pokretanju servera
|
| 24 |
+
# (Ne učitavamo za svaki zahtjev — to bi bilo presporo)
|
| 25 |
+
print("Učitavam model...")
|
| 26 |
+
MODEL, SCALER, FEATURE_NAMES = ucitaj_model()
|
| 27 |
+
|
| 28 |
+
if MODEL is None:
|
| 29 |
+
print("UPOZORENJE: Model nije pronađen.")
|
| 30 |
+
print("Pokreni prvo: python classifier.py")
|
| 31 |
+
else:
|
| 32 |
+
print(f"Model učitan ({len(FEATURE_NAMES)} značajki).")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 36 |
+
# POMOĆNE FUNKCIJE
|
| 37 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 38 |
+
|
| 39 |
+
def greska(poruka: str, status: int = 400):
|
| 40 |
+
"""Vraća JSON odgovor s greškom."""
|
| 41 |
+
return jsonify({"error": poruka}), status
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def izracunaj_slicnost(kod_a: str, kod_b: str) -> float:
|
| 45 |
+
return difflib.SequenceMatcher(None, kod_a.strip(), kod_b.strip()).ratio()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 49 |
+
# RUTE
|
| 50 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 51 |
+
|
| 52 |
+
@app.route("/api/health", methods=["GET"])
|
| 53 |
+
def health():
|
| 54 |
+
"""
|
| 55 |
+
Provjera radi li server i je li model učitan.
|
| 56 |
+
|
| 57 |
+
React frontend poziva ovo pri pokretanju da provjeri
|
| 58 |
+
može li komunicirati sa serverom.
|
| 59 |
+
|
| 60 |
+
Odgovor:
|
| 61 |
+
{
|
| 62 |
+
"status": "ok",
|
| 63 |
+
"model_loaded": true,
|
| 64 |
+
"feature_count": 40
|
| 65 |
+
}
|
| 66 |
+
"""
|
| 67 |
+
return jsonify({
|
| 68 |
+
"status": "ok",
|
| 69 |
+
"model_loaded": MODEL is not None,
|
| 70 |
+
"feature_count": len(FEATURE_NAMES) if FEATURE_NAMES else 0,
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@app.route("/api/analyze", methods=["POST"])
|
| 75 |
+
def analyze():
|
| 76 |
+
"""
|
| 77 |
+
Analizira jedan isječak koda i vraća procjenu AI podrijetla.
|
| 78 |
+
|
| 79 |
+
Zahtjev (JSON):
|
| 80 |
+
{
|
| 81 |
+
"code": "def foo(x): return x", ← obavezno
|
| 82 |
+
"language": "python", ← opcionalno
|
| 83 |
+
"filename": "main.py" ← opcionalno
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
Odgovor (JSON):
|
| 87 |
+
{
|
| 88 |
+
"ai_probability": 0.73,
|
| 89 |
+
"verdict": "Vjerojatno AI",
|
| 90 |
+
"detected_language": "python",
|
| 91 |
+
"top_features": [
|
| 92 |
+
{"name": "avg_identifier_length", "value": 8.5, "importance": 0.084},
|
| 93 |
+
...
|
| 94 |
+
],
|
| 95 |
+
"all_features": { ... },
|
| 96 |
+
"error": null
|
| 97 |
+
}
|
| 98 |
+
"""
|
| 99 |
+
podaci = request.get_json(silent=True)
|
| 100 |
+
if not podaci:
|
| 101 |
+
return greska("Zahtjev mora sadržavati JSON tijelo.")
|
| 102 |
+
|
| 103 |
+
kod = podaci.get("code", "").strip()
|
| 104 |
+
if not kod:
|
| 105 |
+
return greska("Polje 'code' je obavezno i ne smije biti prazno.")
|
| 106 |
+
|
| 107 |
+
if len(kod) > 100_000:
|
| 108 |
+
return greska("Kod je predugačak. Maksimalno 100.000 znakova.")
|
| 109 |
+
|
| 110 |
+
jezik = podaci.get("language")
|
| 111 |
+
datoteka = podaci.get("filename")
|
| 112 |
+
|
| 113 |
+
rezultat = predict(
|
| 114 |
+
code=kod,
|
| 115 |
+
language=jezik,
|
| 116 |
+
filename=datoteka,
|
| 117 |
+
model=MODEL,
|
| 118 |
+
scaler=SCALER,
|
| 119 |
+
feature_names=FEATURE_NAMES,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
return jsonify(rezultat)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@app.route("/api/analyze-batch", methods=["POST"])
|
| 126 |
+
def analyze_batch():
|
| 127 |
+
|
| 128 |
+
podaci = request.get_json(silent=True)
|
| 129 |
+
if not podaci:
|
| 130 |
+
return greska("Zahtjev mora sadržavati JSON tijelo.")
|
| 131 |
+
|
| 132 |
+
submissions = podaci.get("submissions", [])
|
| 133 |
+
if not submissions:
|
| 134 |
+
return greska("Polje 'submissions' je prazno.")
|
| 135 |
+
|
| 136 |
+
if len(submissions) > 200:
|
| 137 |
+
return greska("Maksimalno 200 kodova po zahtjevu.")
|
| 138 |
+
|
| 139 |
+
rezultati = []
|
| 140 |
+
for sub in submissions:
|
| 141 |
+
sub_id = sub.get("id", "nepoznat")
|
| 142 |
+
kod = sub.get("code", "").strip()
|
| 143 |
+
jezik = sub.get("language")
|
| 144 |
+
datoteka = sub.get("filename")
|
| 145 |
+
|
| 146 |
+
if not kod:
|
| 147 |
+
rezultati.append({
|
| 148 |
+
"id": sub_id,
|
| 149 |
+
"error": "Prazni kod."
|
| 150 |
+
})
|
| 151 |
+
continue
|
| 152 |
+
|
| 153 |
+
rez = predict(
|
| 154 |
+
code=kod,
|
| 155 |
+
language=jezik,
|
| 156 |
+
filename=datoteka,
|
| 157 |
+
model=MODEL,
|
| 158 |
+
scaler=SCALER,
|
| 159 |
+
feature_names=FEATURE_NAMES,
|
| 160 |
+
)
|
| 161 |
+
rez["id"] = sub_id
|
| 162 |
+
rezultati.append(rez)
|
| 163 |
+
|
| 164 |
+
# Sažetak za tablični prikaz
|
| 165 |
+
probs = [
|
| 166 |
+
r["ai_probability"]
|
| 167 |
+
for r in rezultati
|
| 168 |
+
if r.get("ai_probability") is not None
|
| 169 |
+
]
|
| 170 |
+
|
| 171 |
+
summary = {
|
| 172 |
+
"total": len(rezultati),
|
| 173 |
+
"high_risk": sum(1 for p in probs if p >= 0.70),
|
| 174 |
+
"medium_risk": sum(1 for p in probs if 0.40 <= p < 0.70),
|
| 175 |
+
"low_risk": sum(1 for p in probs if p < 0.40),
|
| 176 |
+
"avg_ai_probability": round(sum(probs) / len(probs), 4) if probs else 0.0,
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
return jsonify({"results": rezultati, "summary": summary})
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@app.route("/api/similarity", methods=["POST"])
|
| 183 |
+
def similarity():
|
| 184 |
+
podaci = request.get_json(silent=True)
|
| 185 |
+
if not podaci:
|
| 186 |
+
return greska("Zahtjev mora sadržavati JSON tijelo.")
|
| 187 |
+
|
| 188 |
+
submissions = podaci.get("submissions", [])
|
| 189 |
+
if len(submissions) < 2:
|
| 190 |
+
return greska("Potrebna su najmanje 2 koda za usporedbu.")
|
| 191 |
+
|
| 192 |
+
if len(submissions) > 100:
|
| 193 |
+
return greska("Maksimalno 100 kodova po zahtjevu.")
|
| 194 |
+
|
| 195 |
+
ids = [s.get("id", f"kod_{i}") for i, s in enumerate(submissions)]
|
| 196 |
+
codes = [s.get("code", "") for s in submissions]
|
| 197 |
+
n = len(codes)
|
| 198 |
+
|
| 199 |
+
# Izgradi n×n matricu sličnosti
|
| 200 |
+
# matrix[i][j] = sličnost između koda i i koda j
|
| 201 |
+
matrica = [[0.0] * n for _ in range(n)]
|
| 202 |
+
sumnjivi_parovi = []
|
| 203 |
+
|
| 204 |
+
for i in range(n):
|
| 205 |
+
for j in range(n):
|
| 206 |
+
if i == j:
|
| 207 |
+
matrica[i][j] = 1.0
|
| 208 |
+
elif j > i:
|
| 209 |
+
sim = izracunaj_slicnost(codes[i], codes[j])
|
| 210 |
+
matrica[i][j] = round(sim, 4)
|
| 211 |
+
matrica[j][i] = round(sim, 4)
|
| 212 |
+
|
| 213 |
+
# Označi kao sumnjivo ako je sličnost > 70%
|
| 214 |
+
if sim > 0.70:
|
| 215 |
+
sumnjivi_parovi.append({
|
| 216 |
+
"id_a": ids[i],
|
| 217 |
+
"id_b": ids[j],
|
| 218 |
+
"similarity": round(sim, 4),
|
| 219 |
+
})
|
| 220 |
+
|
| 221 |
+
# Sortiraj sumnjive parove po sličnosti (najsličniji prvi)
|
| 222 |
+
sumnjivi_parovi.sort(key=lambda x: x["similarity"], reverse=True)
|
| 223 |
+
|
| 224 |
+
return jsonify({
|
| 225 |
+
"ids": ids,
|
| 226 |
+
"matrix": matrica,
|
| 227 |
+
"suspicious_pairs": sumnjivi_parovi,
|
| 228 |
+
})
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 232 |
+
# POKRETANJE
|
| 233 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 234 |
+
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
print("\n" + "=" * 50)
|
| 237 |
+
print(" AI Code Detector — Backend")
|
| 238 |
+
print("=" * 50)
|
| 239 |
+
print(" Server pokrenut na: http://localhost:5000")
|
| 240 |
+
print(" API rute:")
|
| 241 |
+
print(" GET /api/health")
|
| 242 |
+
print(" POST /api/analyze")
|
| 243 |
+
print(" POST /api/analyze-batch")
|
| 244 |
+
print(" POST /api/similarity")
|
| 245 |
+
print("\n Zaustavi server s Ctrl+C")
|
| 246 |
+
print("=" * 50 + "\n")
|
| 247 |
+
|
| 248 |
+
app.run(
|
| 249 |
+
host="0.0.0.0",
|
| 250 |
+
port=5000,
|
| 251 |
+
debug=True,
|
| 252 |
+
)
|
classifier.py
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
classifier.py
|
| 3 |
+
=============
|
| 4 |
+
Trenira Random Forest klasifikator na datasetu izvučenih značajki,
|
| 5 |
+
evaluira ga i sprema na disk za korištenje u web aplikaciji.
|
| 6 |
+
|
| 7 |
+
Pokretanje (treniranje):
|
| 8 |
+
python classifier.py
|
| 9 |
+
|
| 10 |
+
Korištenje iz drugog fajla (predikcija):
|
| 11 |
+
from classifier import predict
|
| 12 |
+
result = predict(code="def foo(x): return x", language="python")
|
| 13 |
+
print(result["ai_probability"]) # npr. 0.73
|
| 14 |
+
print(result["verdict"]) # "Vjerojatno AI"
|
| 15 |
+
print(result["top_features"]) # koje značajke su bile ključne
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
import csv
|
| 20 |
+
import pickle
|
| 21 |
+
import warnings
|
| 22 |
+
warnings.filterwarnings("ignore")
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 26 |
+
from sklearn.model_selection import train_test_split, cross_val_score
|
| 27 |
+
from sklearn.metrics import (
|
| 28 |
+
classification_report,
|
| 29 |
+
confusion_matrix,
|
| 30 |
+
roc_auc_score,
|
| 31 |
+
)
|
| 32 |
+
from sklearn.preprocessing import StandardScaler
|
| 33 |
+
|
| 34 |
+
from feature_extraction import extract_all_features
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 38 |
+
# KONFIGURACIJA
|
| 39 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 40 |
+
|
| 41 |
+
DATASET_PATH = os.path.join("data", "dataset.csv")
|
| 42 |
+
MODEL_DIR = "model"
|
| 43 |
+
MODEL_PATH = os.path.join(MODEL_DIR, "classifier.pkl")
|
| 44 |
+
SCALER_PATH = os.path.join(MODEL_DIR, "scaler.pkl")
|
| 45 |
+
FEATURES_PATH = os.path.join(MODEL_DIR, "feature_names.pkl")
|
| 46 |
+
|
| 47 |
+
# Kolone koje ne koristimo kao značajke za treniranje
|
| 48 |
+
IGNORED_COLUMNS = {"label", "source", "detected_language", "model_available"}
|
| 49 |
+
|
| 50 |
+
# Značajka perplexity je -1.0 kad model nije učitan — tretiramo kao missing
|
| 51 |
+
PERPLEXITY_MISSING = -1.0
|
| 52 |
+
|
| 53 |
+
# Random Forest parametri
|
| 54 |
+
RF_PARAMS = {
|
| 55 |
+
"n_estimators": 300, # broj stabala — više = stabilnije, ali sporije
|
| 56 |
+
"max_depth": None, # bez ograničenja dubine (stabla rastu do čistih listova)
|
| 57 |
+
"min_samples_leaf": 2, # svaki list mora imati min. 2 primjera (smanjuje overfitting)
|
| 58 |
+
"class_weight": "balanced", # kompenzira neuravnotežene klase (više human nego AI)
|
| 59 |
+
"random_state": 42,
|
| 60 |
+
"n_jobs": -1, # koristi sve dostupne CPU jezgre
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 65 |
+
# UČITAVANJE DATASETA
|
| 66 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 67 |
+
|
| 68 |
+
def ucitaj_dataset(path: str):
|
| 69 |
+
"""
|
| 70 |
+
Učitava dataset.csv i vraća feature matricu X i vektor oznaka y.
|
| 71 |
+
|
| 72 |
+
Tretira -1.0 vrijednosti (perplexity bez modela) kao 0.0
|
| 73 |
+
jer klasifikator ne smije vidjeti negativne vrijednosti kao signal.
|
| 74 |
+
|
| 75 |
+
Parametri:
|
| 76 |
+
path (str): Putanja do CSV datoteke.
|
| 77 |
+
|
| 78 |
+
Vraća:
|
| 79 |
+
X (np.ndarray): Matrica značajki oblika (n_samples, n_features).
|
| 80 |
+
y (np.ndarray): Vektor oznaka (0=human, 1=ai).
|
| 81 |
+
feature_names (list): Nazivi stupaca koji odgovaraju stupcima X.
|
| 82 |
+
"""
|
| 83 |
+
if not os.path.exists(path):
|
| 84 |
+
raise FileNotFoundError(
|
| 85 |
+
f"Dataset nije pronađen na '{path}'.\n"
|
| 86 |
+
f"Pokreni prvo: python download_dataset.py"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
redovi = []
|
| 90 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 91 |
+
reader = csv.DictReader(f)
|
| 92 |
+
for row in reader:
|
| 93 |
+
redovi.append(row)
|
| 94 |
+
|
| 95 |
+
if not redovi:
|
| 96 |
+
raise ValueError("Dataset je prazan.")
|
| 97 |
+
|
| 98 |
+
# Odredi nazive značajki (svi stupci osim ignoriranih)
|
| 99 |
+
sve_kolone = list(redovi[0].keys())
|
| 100 |
+
feature_names = [c for c in sve_kolone if c not in IGNORED_COLUMNS]
|
| 101 |
+
|
| 102 |
+
X_rows = []
|
| 103 |
+
y_list = []
|
| 104 |
+
|
| 105 |
+
for row in redovi:
|
| 106 |
+
try:
|
| 107 |
+
y_list.append(int(row["label"]))
|
| 108 |
+
|
| 109 |
+
# Pretvori svaku značajku u float
|
| 110 |
+
# Perplexity -1.0 → 0.0 (nije dostupan, ne smije biti signal)
|
| 111 |
+
vrijednosti = []
|
| 112 |
+
for feat in feature_names:
|
| 113 |
+
val = float(row[feat])
|
| 114 |
+
if feat == "perplexity" and val == PERPLEXITY_MISSING:
|
| 115 |
+
val = 0.0
|
| 116 |
+
vrijednosti.append(val)
|
| 117 |
+
|
| 118 |
+
X_rows.append(vrijednosti)
|
| 119 |
+
|
| 120 |
+
except (ValueError, KeyError):
|
| 121 |
+
continue # preskoči neispravne retke
|
| 122 |
+
|
| 123 |
+
X = np.array(X_rows, dtype=np.float32)
|
| 124 |
+
y = np.array(y_list, dtype=np.int32)
|
| 125 |
+
|
| 126 |
+
print(f" Učitano {len(y)} primjera, {len(feature_names)} značajki")
|
| 127 |
+
print(f" Human (0): {sum(y == 0)} | AI (1): {sum(y == 1)}")
|
| 128 |
+
|
| 129 |
+
return X, y, feature_names
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ──────────���──────────────────────────────────────────────────────────────────
|
| 133 |
+
# TRENIRANJE
|
| 134 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 135 |
+
|
| 136 |
+
def treniraj(X, y, feature_names):
|
| 137 |
+
"""
|
| 138 |
+
Trenira Random Forest klasifikator i vraća trenirani model zajedno
|
| 139 |
+
sa scalerom i rezultatima evaluacije.
|
| 140 |
+
|
| 141 |
+
Pipeline:
|
| 142 |
+
1. Podijeli podatke 80% trening / 20% test
|
| 143 |
+
2. Normalizira značajke (StandardScaler)
|
| 144 |
+
3. Trenira Random Forest
|
| 145 |
+
4. Evaluira na test skupu
|
| 146 |
+
5. Pokreće 5-fold cross-validation za pouzdaniju procjenu
|
| 147 |
+
|
| 148 |
+
Parametri:
|
| 149 |
+
X (np.ndarray): Matrica značajki.
|
| 150 |
+
y (np.ndarray): Vektor oznaka.
|
| 151 |
+
feature_names (list): Nazivi značajki.
|
| 152 |
+
|
| 153 |
+
Vraća:
|
| 154 |
+
model: Trenirani RandomForestClassifier.
|
| 155 |
+
scaler: Trenirani StandardScaler.
|
| 156 |
+
metrics: Rječnik s metrikama evaluacije.
|
| 157 |
+
"""
|
| 158 |
+
# 1. Podjela na trening i test skup
|
| 159 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 160 |
+
X, y, test_size=0.2, random_state=42, stratify=y
|
| 161 |
+
# stratify=y osigurava da i trening i test imaju isti omjer klasa
|
| 162 |
+
)
|
| 163 |
+
print(f"\n Trening: {len(y_train)} primjera")
|
| 164 |
+
print(f" Test: {len(y_test)} primjera")
|
| 165 |
+
|
| 166 |
+
# 2. Normalizacija — StandardScaler svaku značajku svede na
|
| 167 |
+
# srednju vrijednost 0 i standardnu devijaciju 1.
|
| 168 |
+
# VAŽNO: scaler se fitira SAMO na trening skupu,
|
| 169 |
+
# a transformira i trening i test (da ne bi "curilo" znanje)
|
| 170 |
+
scaler = StandardScaler()
|
| 171 |
+
X_train_scaled = scaler.fit_transform(X_train)
|
| 172 |
+
X_test_scaled = scaler.transform(X_test)
|
| 173 |
+
|
| 174 |
+
# 3. Treniranje Random Foresta
|
| 175 |
+
print("\n Treniram Random Forest...")
|
| 176 |
+
model = RandomForestClassifier(**RF_PARAMS)
|
| 177 |
+
model.fit(X_train_scaled, y_train)
|
| 178 |
+
|
| 179 |
+
# 4. Evaluacija na test skupu
|
| 180 |
+
y_pred = model.predict(X_test_scaled)
|
| 181 |
+
y_pred_prob = model.predict_proba(X_test_scaled)[:, 1]
|
| 182 |
+
|
| 183 |
+
print("\n" + "─" * 50)
|
| 184 |
+
print(" REZULTATI EVALUACIJE")
|
| 185 |
+
print("─" * 50)
|
| 186 |
+
print(classification_report(
|
| 187 |
+
y_test, y_pred,
|
| 188 |
+
target_names=["Human (0)", "AI (1)"],
|
| 189 |
+
digits=3
|
| 190 |
+
))
|
| 191 |
+
|
| 192 |
+
# Matrica zabune — pokazuje lažno pozitivne i lažno negativne
|
| 193 |
+
cm = confusion_matrix(y_test, y_pred)
|
| 194 |
+
tn, fp, fn, tp = cm.ravel()
|
| 195 |
+
print(f" Matrica zabune:")
|
| 196 |
+
print(f" Ispravno human: {tn} (true negative)")
|
| 197 |
+
print(f" Lažno označen AI: {fp} (false positive)")
|
| 198 |
+
print(f" Propušten AI: {fn} (false negative)")
|
| 199 |
+
print(f" Ispravno AI: {tp} (true positive)\n")
|
| 200 |
+
|
| 201 |
+
# AUC-ROC — mjera kvalitete rankiranja (0.5=slučajno, 1.0=savršeno)
|
| 202 |
+
auc = roc_auc_score(y_test, y_pred_prob)
|
| 203 |
+
print(f" AUC-ROC: {auc:.4f}")
|
| 204 |
+
|
| 205 |
+
# 5. Cross-validation — pouzdanija procjena jer trenira/testira 5 puta
|
| 206 |
+
print("\n 5-fold cross-validation (može potrajati minutu)...")
|
| 207 |
+
X_scaled_full = scaler.transform(X)
|
| 208 |
+
cv_scores = cross_val_score(
|
| 209 |
+
model, X_scaled_full, y,
|
| 210 |
+
cv=5, scoring="f1", n_jobs=-1
|
| 211 |
+
)
|
| 212 |
+
print(f" CV F1 scores: {[f'{s:.3f}' for s in cv_scores]}")
|
| 213 |
+
print(f" CV F1 prosjek: {cv_scores.mean():.3f} "
|
| 214 |
+
f"(±{cv_scores.std():.3f})")
|
| 215 |
+
|
| 216 |
+
# Top 10 najvažnijih značajki
|
| 217 |
+
importances = model.feature_importances_
|
| 218 |
+
top_idx = np.argsort(importances)[::-1][:10]
|
| 219 |
+
print("\n Top 10 najvažnijih značajki:")
|
| 220 |
+
for rank, idx in enumerate(top_idx, 1):
|
| 221 |
+
print(f" {rank:2}. {feature_names[idx]:<38} {importances[idx]:.4f}")
|
| 222 |
+
|
| 223 |
+
metrics = {
|
| 224 |
+
"auc_roc": auc,
|
| 225 |
+
"cv_f1_mean": cv_scores.mean(),
|
| 226 |
+
"cv_f1_std": cv_scores.std(),
|
| 227 |
+
"true_negative": int(tn),
|
| 228 |
+
"false_positive": int(fp),
|
| 229 |
+
"false_negative": int(fn),
|
| 230 |
+
"true_positive": int(tp),
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
return model, scaler, metrics
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 237 |
+
# SPREMANJE MODELA
|
| 238 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 239 |
+
|
| 240 |
+
def spremi_model(model, scaler, feature_names):
|
| 241 |
+
"""
|
| 242 |
+
Sprema trenirani model, scaler i listu naziva značajki na disk.
|
| 243 |
+
|
| 244 |
+
Sva tri fajla su potrebna za predikciju:
|
| 245 |
+
- model : donosi odluku
|
| 246 |
+
- scaler : normalizira ulaz na isti način kao pri treniranju
|
| 247 |
+
- feature_names : osigurava da se značajke šalju u ispravnom redoslijedu
|
| 248 |
+
|
| 249 |
+
Parametri:
|
| 250 |
+
model: Trenirani RandomForestClassifier.
|
| 251 |
+
scaler: Trenirani StandardScaler.
|
| 252 |
+
feature_names: Lista naziva značajki.
|
| 253 |
+
"""
|
| 254 |
+
os.makedirs(MODEL_DIR, exist_ok=True)
|
| 255 |
+
|
| 256 |
+
with open(MODEL_PATH, "wb") as f: pickle.dump(model, f)
|
| 257 |
+
with open(SCALER_PATH, "wb") as f: pickle.dump(scaler, f)
|
| 258 |
+
with open(FEATURES_PATH, "wb") as f: pickle.dump(feature_names, f)
|
| 259 |
+
|
| 260 |
+
print(f"\n Model spremljen u: {MODEL_PATH}")
|
| 261 |
+
print(f" Scaler spremljen u: {SCALER_PATH}")
|
| 262 |
+
print(f" Nazivi značajki spremljeni: {FEATURES_PATH}")
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 266 |
+
# PREDIKCIJA — koristi se iz web aplikacije
|
| 267 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 268 |
+
|
| 269 |
+
def ucitaj_model():
|
| 270 |
+
"""
|
| 271 |
+
Učitava model, scaler i nazive značajki s diska.
|
| 272 |
+
Poziva se jednom pri pokretanju web servera.
|
| 273 |
+
|
| 274 |
+
Vraća:
|
| 275 |
+
(model, scaler, feature_names) ili (None, None, None) ako model ne postoji.
|
| 276 |
+
"""
|
| 277 |
+
if not all(os.path.exists(p) for p in [MODEL_PATH, SCALER_PATH, FEATURES_PATH]):
|
| 278 |
+
return None, None, None
|
| 279 |
+
|
| 280 |
+
with open(MODEL_PATH, "rb") as f: model = pickle.load(f)
|
| 281 |
+
with open(SCALER_PATH, "rb") as f: scaler = pickle.load(f)
|
| 282 |
+
with open(FEATURES_PATH, "rb") as f: feature_names = pickle.load(f)
|
| 283 |
+
|
| 284 |
+
return model, scaler, feature_names
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 289 |
+
# GENERIRANJE OBJAŠNJENJA
|
| 290 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 291 |
+
|
| 292 |
+
def generate_explanations(features: dict, ai_prob: float) -> list:
|
| 293 |
+
"""
|
| 294 |
+
Generira listu objašnjenja na engleskom jeziku koja opisuju
|
| 295 |
+
zašto kod izgleda AI generiran ili čovječji.
|
| 296 |
+
|
| 297 |
+
Svako objašnjenje je rječnik s:
|
| 298 |
+
"text" — rečenica objašnjenja
|
| 299 |
+
"severity" — "high" | "medium" | "low" | "positive"
|
| 300 |
+
"feature" — naziv značajke na koju se objašnjenje odnosi
|
| 301 |
+
|
| 302 |
+
Pragovi su kalibrirani na temelju tipičnih vrijednosti u
|
| 303 |
+
AI-Detector i HMCorp datasetovima.
|
| 304 |
+
|
| 305 |
+
Parametri:
|
| 306 |
+
features (dict): Rječnik značajki iz extract_all_features().
|
| 307 |
+
ai_prob (float): Vjerojatnost AI podrijetla (0.0 – 1.0).
|
| 308 |
+
|
| 309 |
+
Vraća:
|
| 310 |
+
list: Lista rječnika s objašnjenjima, sortirana po ozbiljnosti.
|
| 311 |
+
"""
|
| 312 |
+
objasnjenja = []
|
| 313 |
+
|
| 314 |
+
def dodaj(text, severity, feature):
|
| 315 |
+
objasnjenja.append({"text": text, "severity": severity, "feature": feature})
|
| 316 |
+
|
| 317 |
+
# ── IMENOVANJE ─────────────────────────────────────────────────────────
|
| 318 |
+
|
| 319 |
+
avg_id_len = features.get("avg_identifier_length", 0)
|
| 320 |
+
if avg_id_len > 7.5:
|
| 321 |
+
dodaj(
|
| 322 |
+
f"Identifier names are unusually long and descriptive "
|
| 323 |
+
f"(average {avg_id_len:.1f} characters). "
|
| 324 |
+
f"AI-generated code consistently favours verbose, self-documenting names "
|
| 325 |
+
f"such as 'calculate_average_value' over typical student shorthand like 'avg'.",
|
| 326 |
+
"high", "avg_identifier_length"
|
| 327 |
+
)
|
| 328 |
+
elif avg_id_len > 5.5:
|
| 329 |
+
dodaj(
|
| 330 |
+
f"Identifier names are moderately long (average {avg_id_len:.1f} characters), "
|
| 331 |
+
f"which is slightly above the typical range for human-written student code.",
|
| 332 |
+
"medium", "avg_identifier_length"
|
| 333 |
+
)
|
| 334 |
+
elif avg_id_len < 2.5 and avg_id_len > 0:
|
| 335 |
+
dodaj(
|
| 336 |
+
f"Identifier names are very short (average {avg_id_len:.1f} characters), "
|
| 337 |
+
f"consistent with a human programmer's preference for concise variable names.",
|
| 338 |
+
"positive", "avg_identifier_length"
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
naming_cons = features.get("naming_consistency", 0)
|
| 342 |
+
if naming_cons > 0.85:
|
| 343 |
+
dodaj(
|
| 344 |
+
f"Naming convention is highly consistent throughout the submission "
|
| 345 |
+
f"({naming_cons * 100:.0f}% of identifiers follow the same pattern). "
|
| 346 |
+
f"Human programmers typically mix conventions, especially in longer submissions.",
|
| 347 |
+
"high", "naming_consistency"
|
| 348 |
+
)
|
| 349 |
+
elif naming_cons < 0.3 and naming_cons > 0:
|
| 350 |
+
dodaj(
|
| 351 |
+
f"Naming convention varies across the submission, which is characteristic "
|
| 352 |
+
f"of code written incrementally by a human programmer.",
|
| 353 |
+
"positive", "naming_consistency"
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
single_char = features.get("single_char_name_ratio", 0)
|
| 357 |
+
if single_char < 0.03 and features.get("num_functions", 0) > 1:
|
| 358 |
+
dodaj(
|
| 359 |
+
f"No single-character variable names were detected. "
|
| 360 |
+
f"Human programmers routinely use short names such as 'i', 'x', or 'n' "
|
| 361 |
+
f"in loops and helper functions; their absence is atypical.",
|
| 362 |
+
"medium", "single_char_name_ratio"
|
| 363 |
+
)
|
| 364 |
+
elif single_char > 0.25:
|
| 365 |
+
dodaj(
|
| 366 |
+
f"A notable proportion of variables use single-character names "
|
| 367 |
+
f"({single_char * 100:.0f}%), which is common in human-written code.",
|
| 368 |
+
"positive", "single_char_name_ratio"
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
# ── KOMENTARI I DOCSTRINGOVI ───────────────────────────────────────────
|
| 372 |
+
|
| 373 |
+
comment_ratio = features.get("comment_ratio", 0)
|
| 374 |
+
if comment_ratio > 0.30:
|
| 375 |
+
dodaj(
|
| 376 |
+
f"Comment density is substantially above average — "
|
| 377 |
+
f"{comment_ratio * 100:.0f}% of lines contain inline comments. "
|
| 378 |
+
f"AI models tend to annotate nearly every logical step, "
|
| 379 |
+
f"whereas students typically comment only non-obvious sections.",
|
| 380 |
+
"high", "comment_ratio"
|
| 381 |
+
)
|
| 382 |
+
elif comment_ratio > 0.18:
|
| 383 |
+
dodaj(
|
| 384 |
+
f"Comment density ({comment_ratio * 100:.0f}% of lines) is higher than "
|
| 385 |
+
f"typically observed in student submissions at this level.",
|
| 386 |
+
"medium", "comment_ratio"
|
| 387 |
+
)
|
| 388 |
+
elif comment_ratio < 0.03:
|
| 389 |
+
dodaj(
|
| 390 |
+
f"Very few or no inline comments are present, which is more consistent "
|
| 391 |
+
f"with human-written code at this stage of the course.",
|
| 392 |
+
"positive", "comment_ratio"
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
num_docs = features.get("num_docstrings", 0)
|
| 396 |
+
num_fns = features.get("num_functions", 1)
|
| 397 |
+
if num_docs > 0 and num_fns > 0:
|
| 398 |
+
doc_coverage = num_docs / num_fns
|
| 399 |
+
if doc_coverage >= 0.9:
|
| 400 |
+
dodaj(
|
| 401 |
+
f"Every function in the submission includes a formal docstring "
|
| 402 |
+
f"({num_docs} of {num_fns} functions documented). "
|
| 403 |
+
f"Complete docstring coverage is a strong marker of AI-generated code; "
|
| 404 |
+
f"students rarely document all functions unless explicitly required.",
|
| 405 |
+
"high", "num_docstrings"
|
| 406 |
+
)
|
| 407 |
+
elif doc_coverage >= 0.5:
|
| 408 |
+
dodaj(
|
| 409 |
+
f"More than half of the functions include docstrings "
|
| 410 |
+
f"({num_docs} of {num_fns}), which is above the student average.",
|
| 411 |
+
"medium", "num_docstrings"
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
# ── STRUKTURNE ZNAČAJKE ────────────────────────────────────────────────
|
| 415 |
+
|
| 416 |
+
avg_fn_len = features.get("avg_function_length", 0)
|
| 417 |
+
if avg_fn_len > 20:
|
| 418 |
+
dodaj(
|
| 419 |
+
f"Functions are notably long on average ({avg_fn_len:.0f} lines). "
|
| 420 |
+
f"AI models tend to produce complete, self-contained implementations; "
|
| 421 |
+
f"students more often break logic across multiple smaller functions "
|
| 422 |
+
f"or leave parts incomplete.",
|
| 423 |
+
"medium", "avg_function_length"
|
| 424 |
+
)
|
| 425 |
+
elif avg_fn_len > 0 and avg_fn_len < 5:
|
| 426 |
+
dodaj(
|
| 427 |
+
f"Functions are very short on average ({avg_fn_len:.1f} lines), "
|
| 428 |
+
f"which may indicate a human programmer's incremental coding style.",
|
| 429 |
+
"positive", "avg_function_length"
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
try_density = features.get("try_density", 0)
|
| 433 |
+
if try_density > 0.06:
|
| 434 |
+
dodaj(
|
| 435 |
+
f"The submission contains a relatively high density of try/except blocks. "
|
| 436 |
+
f"Comprehensive error handling across all edge cases is a pattern "
|
| 437 |
+
f"commonly exhibited by AI generators, which anticipate and handle "
|
| 438 |
+
f"exceptions that students typically overlook.",
|
| 439 |
+
"medium", "try_density"
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
nesting = features.get("max_nesting_depth", 0)
|
| 443 |
+
if nesting > 5:
|
| 444 |
+
dodaj(
|
| 445 |
+
f"Code nesting reaches a depth of {int(nesting)} levels. "
|
| 446 |
+
f"While not conclusive, deeply nested logic can reflect an AI model's "
|
| 447 |
+
f"tendency to handle all conditional branches explicitly.",
|
| 448 |
+
"medium", "max_nesting_depth"
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
# ── STATISTIČKA ANALIZA ────────────────────────────────────────────────
|
| 452 |
+
|
| 453 |
+
token_entropy = features.get("token_entropy", 0)
|
| 454 |
+
if token_entropy > 0 and token_entropy < 3.8:
|
| 455 |
+
dodaj(
|
| 456 |
+
f"Token entropy is low ({token_entropy:.2f}), indicating that the vocabulary "
|
| 457 |
+
f"of the submission is repetitive and predictable. "
|
| 458 |
+
f"This is consistent with language model output, which tends to reuse "
|
| 459 |
+
f"the same phrasing and structural patterns.",
|
| 460 |
+
"high", "token_entropy"
|
| 461 |
+
)
|
| 462 |
+
elif token_entropy > 5.5:
|
| 463 |
+
dodaj(
|
| 464 |
+
f"Token entropy is relatively high ({token_entropy:.2f}), suggesting "
|
| 465 |
+
f"a diverse and varied vocabulary more typical of human authorship.",
|
| 466 |
+
"positive", "token_entropy"
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
perplexity = features.get("perplexity", -1)
|
| 470 |
+
if perplexity != -1 and perplexity > 0:
|
| 471 |
+
if perplexity < 8:
|
| 472 |
+
dodaj(
|
| 473 |
+
f"The code's perplexity score is very low ({perplexity:.1f}), meaning "
|
| 474 |
+
f"a language model finds the token sequence highly predictable. "
|
| 475 |
+
f"This strongly suggests the code was generated by a similar model.",
|
| 476 |
+
"high", "perplexity"
|
| 477 |
+
)
|
| 478 |
+
elif perplexity < 20:
|
| 479 |
+
dodaj(
|
| 480 |
+
f"Perplexity ({perplexity:.1f}) falls within a range that is "
|
| 481 |
+
f"moderately consistent with AI-generated code.",
|
| 482 |
+
"medium", "perplexity"
|
| 483 |
+
)
|
| 484 |
+
elif perplexity > 50:
|
| 485 |
+
dodaj(
|
| 486 |
+
f"Perplexity is high ({perplexity:.1f}), indicating the code "
|
| 487 |
+
f"contains patterns that a language model would consider unexpected — "
|
| 488 |
+
f"a characteristic of human authorship.",
|
| 489 |
+
"positive", "perplexity"
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# ── FORMATIRANJE ───────────────────────────────────────────────────────
|
| 493 |
+
|
| 494 |
+
trailing = features.get("trailing_whitespace_ratio", 0)
|
| 495 |
+
if trailing > 0.15:
|
| 496 |
+
dodaj(
|
| 497 |
+
f"A notable proportion of lines contain trailing whitespace "
|
| 498 |
+
f"({trailing * 100:.0f}%), which is typical of code edited by hand "
|
| 499 |
+
f"and inconsistent with AI-generated output.",
|
| 500 |
+
"positive", "trailing_whitespace_ratio"
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
op_cons = features.get("operator_spacing_consistency", 0)
|
| 504 |
+
if op_cons > 0.95:
|
| 505 |
+
dodaj(
|
| 506 |
+
f"Spacing around operators is perfectly consistent throughout the submission. "
|
| 507 |
+
f"AI models apply style conventions uniformly; human programmers "
|
| 508 |
+
f"occasionally deviate, particularly under time pressure.",
|
| 509 |
+
"medium", "operator_spacing_consistency"
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
# Ako nema signala, dodaj neutralnu poruku
|
| 513 |
+
if not objasnjenja:
|
| 514 |
+
if ai_prob > 0.5:
|
| 515 |
+
dodaj(
|
| 516 |
+
"No single dominant signal was identified; the classification is based "
|
| 517 |
+
"on a combination of subtle stylistic and structural patterns.",
|
| 518 |
+
"medium", "combined"
|
| 519 |
+
)
|
| 520 |
+
else:
|
| 521 |
+
dodaj(
|
| 522 |
+
"No strong AI-generation markers were detected. "
|
| 523 |
+
"The submission's style and structure are consistent with human authorship.",
|
| 524 |
+
"positive", "combined"
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
# Sortiraj: high → medium → positive/low
|
| 528 |
+
priority = {"high": 0, "medium": 1, "low": 2, "positive": 3}
|
| 529 |
+
objasnjenja.sort(key=lambda x: priority.get(x["severity"], 2))
|
| 530 |
+
|
| 531 |
+
return objasnjenja
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def predict(code: str, language: str = None, filename: str = None,
|
| 535 |
+
model=None, scaler=None, feature_names=None) -> dict:
|
| 536 |
+
"""
|
| 537 |
+
Analizira isječak koda i vraća procjenu vjerojatnosti AI podrijetla.
|
| 538 |
+
|
| 539 |
+
Ako model/scaler/feature_names nisu proslijeđeni, automatski ih učita s diska.
|
| 540 |
+
|
| 541 |
+
Parametri:
|
| 542 |
+
code (str): Izvorni kod za analizu.
|
| 543 |
+
language (str): Programski jezik (opcionalno, automatska detekcija).
|
| 544 |
+
filename (str): Ime datoteke (opcionalno, pomaže detekciji jezika).
|
| 545 |
+
model: Učitani model (opcionalno, za višekratnu upotrebu).
|
| 546 |
+
scaler: Učitani scaler (opcionalno).
|
| 547 |
+
feature_names (list):Lista naziva značajki (opcionalno).
|
| 548 |
+
|
| 549 |
+
Vraća:
|
| 550 |
+
dict s ključevima:
|
| 551 |
+
"ai_probability" – float 0.0-1.0, vjerojatnost AI podrijetla
|
| 552 |
+
"verdict" – string s tumačenjem rezultata
|
| 553 |
+
"detected_language" – prepoznati programski jezik
|
| 554 |
+
"top_features" – lista (naziv, vrijednost) top 5 značajki
|
| 555 |
+
"all_features" – rječnik svih izvučenih značajki
|
| 556 |
+
"error" – string s greškom, ili None ako je sve OK
|
| 557 |
+
"""
|
| 558 |
+
# Učitaj model ako nije proslijeđen
|
| 559 |
+
if model is None:
|
| 560 |
+
model, scaler, feature_names = ucitaj_model()
|
| 561 |
+
|
| 562 |
+
if model is None:
|
| 563 |
+
return {
|
| 564 |
+
"ai_probability": None,
|
| 565 |
+
"verdict": "Model nije dostupan",
|
| 566 |
+
"detected_language": None,
|
| 567 |
+
"top_features": [],
|
| 568 |
+
"all_features": {},
|
| 569 |
+
"error": "Model nije treniran. Pokreni: python classifier.py"
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
# Izvuci značajke
|
| 573 |
+
sve_znacajke = extract_all_features(
|
| 574 |
+
code=code, language=language, filename=filename
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
# Složi feature vektor u TOČNO isti redosljed kao pri treniranju
|
| 578 |
+
feature_vector = []
|
| 579 |
+
for feat in feature_names:
|
| 580 |
+
val = sve_znacajke.get(feat, 0.0)
|
| 581 |
+
if feat == "perplexity" and val == PERPLEXITY_MISSING:
|
| 582 |
+
val = 0.0
|
| 583 |
+
feature_vector.append(float(val))
|
| 584 |
+
|
| 585 |
+
X = np.array([feature_vector], dtype=np.float32)
|
| 586 |
+
X_scaled = scaler.transform(X)
|
| 587 |
+
|
| 588 |
+
# Predikcija
|
| 589 |
+
ai_prob = float(model.predict_proba(X_scaled)[0][1])
|
| 590 |
+
|
| 591 |
+
# Tumačenje
|
| 592 |
+
if ai_prob >= 0.80:
|
| 593 |
+
verdict = "Vjerojatno AI"
|
| 594 |
+
elif ai_prob >= 0.60:
|
| 595 |
+
verdict = "Moguće AI"
|
| 596 |
+
elif ai_prob >= 0.40:
|
| 597 |
+
verdict = "Nejasno"
|
| 598 |
+
elif ai_prob >= 0.20:
|
| 599 |
+
verdict = "Moguće čovječji"
|
| 600 |
+
else:
|
| 601 |
+
verdict = "Vjerojatno čovječji"
|
| 602 |
+
|
| 603 |
+
# Top 5 značajki koje su doprinijele odluci
|
| 604 |
+
importances = model.feature_importances_
|
| 605 |
+
top_idx = np.argsort(importances)[::-1][:5]
|
| 606 |
+
top_features = [
|
| 607 |
+
{
|
| 608 |
+
"name": feature_names[i],
|
| 609 |
+
"value": round(feature_vector[i], 4),
|
| 610 |
+
"importance": round(float(importances[i]), 4),
|
| 611 |
+
}
|
| 612 |
+
for i in top_idx
|
| 613 |
+
]
|
| 614 |
+
|
| 615 |
+
# Generiraj objašnjenja zašto je kod klasificiran ovako
|
| 616 |
+
objasnjenja = generate_explanations(sve_znacajke, ai_prob)
|
| 617 |
+
|
| 618 |
+
return {
|
| 619 |
+
"ai_probability": round(ai_prob, 4),
|
| 620 |
+
"verdict": verdict,
|
| 621 |
+
"detected_language": sve_znacajke.get("detected_language", "nepoznat"),
|
| 622 |
+
"top_features": top_features,
|
| 623 |
+
"all_features": sve_znacajke,
|
| 624 |
+
"explanations": objasnjenja,
|
| 625 |
+
"error": None,
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 630 |
+
# GLAVNI PROGRAM
|
| 631 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 632 |
+
|
| 633 |
+
def main():
|
| 634 |
+
print("=" * 50)
|
| 635 |
+
print(" Treniranje klasifikatora")
|
| 636 |
+
print("=" * 50)
|
| 637 |
+
|
| 638 |
+
# Učitaj dataset
|
| 639 |
+
print(f"\n Učitavam dataset iz '{DATASET_PATH}'...")
|
| 640 |
+
X, y, feature_names = ucitaj_dataset(DATASET_PATH)
|
| 641 |
+
|
| 642 |
+
# Treniraj
|
| 643 |
+
model, scaler, metrics = treniraj(X, y, feature_names)
|
| 644 |
+
|
| 645 |
+
# Spremi
|
| 646 |
+
spremi_model(model, scaler, feature_names)
|
| 647 |
+
|
| 648 |
+
# Brzi test predikcije
|
| 649 |
+
print("\n" + "─" * 50)
|
| 650 |
+
print(" BRZI TEST PREDIKCIJE")
|
| 651 |
+
print("─" * 50)
|
| 652 |
+
|
| 653 |
+
test_kodovi = {
|
| 654 |
+
"AI Python": '''
|
| 655 |
+
def calculate_fibonacci(n: int) -> list:
|
| 656 |
+
"""
|
| 657 |
+
Generate a Fibonacci sequence up to n terms.
|
| 658 |
+
|
| 659 |
+
Args:
|
| 660 |
+
n: The number of terms to generate.
|
| 661 |
+
|
| 662 |
+
Returns:
|
| 663 |
+
A list containing the Fibonacci sequence.
|
| 664 |
+
"""
|
| 665 |
+
if n <= 0:
|
| 666 |
+
raise ValueError("Number of terms must be positive.")
|
| 667 |
+
fibonacci_sequence = [0, 1]
|
| 668 |
+
for i in range(2, n):
|
| 669 |
+
next_value = fibonacci_sequence[i - 1] + fibonacci_sequence[i - 2]
|
| 670 |
+
fibonacci_sequence.append(next_value)
|
| 671 |
+
return fibonacci_sequence[:n]
|
| 672 |
+
''',
|
| 673 |
+
"Human Python": '''
|
| 674 |
+
def fib(n):
|
| 675 |
+
# quick fib
|
| 676 |
+
a, b = 0, 1
|
| 677 |
+
res = []
|
| 678 |
+
for _ in range(n):
|
| 679 |
+
res.append(a)
|
| 680 |
+
a, b = b, a+b
|
| 681 |
+
return res
|
| 682 |
+
''',
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
for naziv, kod in test_kodovi.items():
|
| 686 |
+
rezultat = predict(kod, model=model, scaler=scaler,
|
| 687 |
+
feature_names=feature_names)
|
| 688 |
+
prob = rezultat["ai_probability"]
|
| 689 |
+
verdict = rezultat["verdict"]
|
| 690 |
+
lang = rezultat["detected_language"]
|
| 691 |
+
print(f"\n [{naziv}]")
|
| 692 |
+
print(f" Jezik: {lang}")
|
| 693 |
+
print(f" AI vjerojatnost: {prob:.1%}")
|
| 694 |
+
print(f" Zaključak: {verdict}")
|
| 695 |
+
print(f" Ključne značajke:")
|
| 696 |
+
for feat in rezultat["top_features"]:
|
| 697 |
+
print(f" {feat['name']:<35} vrijednost={feat['value']:.4f}")
|
| 698 |
+
|
| 699 |
+
print("\n" + "=" * 50)
|
| 700 |
+
print(" Treniranje završeno.")
|
| 701 |
+
print(" Sljedeći korak: python app.py")
|
| 702 |
+
print("=" * 50)
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
if __name__ == "__main__":
|
| 706 |
+
main()
|
data/dataset.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
download_dataset.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import csv
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from datasets import load_dataset
|
| 6 |
+
|
| 7 |
+
from feature_extraction import extract_all_features
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 11 |
+
# KONFIGURACIJA
|
| 12 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 13 |
+
|
| 14 |
+
OUTPUT_DIR = "data"
|
| 15 |
+
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "dataset.csv")
|
| 16 |
+
|
| 17 |
+
# Koliko primjera uzeti po datasetu (None = sve)
|
| 18 |
+
# Za prvi brzi test stavi npr. 500, za pravo treniranje None
|
| 19 |
+
MAX_SAMPLES_PER_DATASET = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 23 |
+
# PARSERI ZA SVAKI DATASET
|
| 24 |
+
# Svaki dataset ima svoju strukturu — ove funkcije je normaliziraju
|
| 25 |
+
# u isti format: lista rječnika s ključevima "code", "label", "language"
|
| 26 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 27 |
+
|
| 28 |
+
def parse_aigcodeset(max_samples=None):
|
| 29 |
+
print(" Skidanje AIGCodeSet dataseta...")
|
| 30 |
+
try:
|
| 31 |
+
ds = load_dataset("basakdemirok/AIGCodeSet", split="train")
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f" [GREŠKA] Nije moguće skinuti AIGCodeSet: {e}")
|
| 34 |
+
return []
|
| 35 |
+
|
| 36 |
+
primjeri = []
|
| 37 |
+
for row in ds:
|
| 38 |
+
# Izvuci kod — provjeri moguće nazive kolona
|
| 39 |
+
code = row.get("code") or row.get("source_code") or row.get("content") or ""
|
| 40 |
+
if not code or not code.strip():
|
| 41 |
+
continue
|
| 42 |
+
|
| 43 |
+
# Odredi oznaku — sve što nije "human" je AI
|
| 44 |
+
raw_label = str(row.get("label", "")).lower()
|
| 45 |
+
if "human" in raw_label:
|
| 46 |
+
label = 0 # ljudski kod
|
| 47 |
+
else:
|
| 48 |
+
label = 1 # AI generirani kod
|
| 49 |
+
|
| 50 |
+
primjeri.append({
|
| 51 |
+
"code": code,
|
| 52 |
+
"label": label,
|
| 53 |
+
"language": "python", # AIGCodeSet je samo Python
|
| 54 |
+
"source": "AIGCodeSet",
|
| 55 |
+
})
|
| 56 |
+
|
| 57 |
+
if max_samples and len(primjeri) >= max_samples:
|
| 58 |
+
break
|
| 59 |
+
|
| 60 |
+
human_count = sum(1 for p in primjeri if p["label"] == 0)
|
| 61 |
+
ai_count = sum(1 for p in primjeri if p["label"] == 1)
|
| 62 |
+
print(f" Učitano {len(primjeri)} primjera "
|
| 63 |
+
f"({human_count} human, {ai_count} AI)")
|
| 64 |
+
return primjeri
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def parse_ai_code_detection(max_samples=None):
|
| 68 |
+
print(" Skidanje ai-code-detection dataseta...")
|
| 69 |
+
try:
|
| 70 |
+
ds = load_dataset("serafeimdossas/ai-code-detection", split="train")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f" [GREŠKA] Nije moguće skinuti ai-code-detection: {e}")
|
| 73 |
+
return []
|
| 74 |
+
|
| 75 |
+
primjeri = []
|
| 76 |
+
for row in ds:
|
| 77 |
+
code = row.get("code") or row.get("solution") or row.get("content") or ""
|
| 78 |
+
if not code or not code.strip():
|
| 79 |
+
continue
|
| 80 |
+
|
| 81 |
+
raw_label = str(row.get("label", "")).lower()
|
| 82 |
+
if "human" in raw_label:
|
| 83 |
+
label = 0
|
| 84 |
+
else:
|
| 85 |
+
label = 1
|
| 86 |
+
|
| 87 |
+
primjeri.append({
|
| 88 |
+
"code": code,
|
| 89 |
+
"label": label,
|
| 90 |
+
"language": "python",
|
| 91 |
+
"source": "ai-code-detection",
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
if max_samples and len(primjeri) >= max_samples:
|
| 95 |
+
break
|
| 96 |
+
|
| 97 |
+
human_count = sum(1 for p in primjeri if p["label"] == 0)
|
| 98 |
+
ai_count = sum(1 for p in primjeri if p["label"] == 1)
|
| 99 |
+
print(f" Učitano {len(primjeri)} primjera "
|
| 100 |
+
f"({human_count} human, {ai_count} AI)")
|
| 101 |
+
return primjeri
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 105 |
+
# IZVLAČENJE ZNAČAJKI I SPREMANJE U CSV
|
| 106 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 107 |
+
|
| 108 |
+
def procesiraj_i_spremi(primjeri: list, output_path: str) -> None:
|
| 109 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 110 |
+
|
| 111 |
+
uspjesno = 0
|
| 112 |
+
greske = 0
|
| 113 |
+
pisac = None
|
| 114 |
+
f = None
|
| 115 |
+
|
| 116 |
+
print(f"\n Izvlačim značajke iz {len(primjeri)} primjera...")
|
| 117 |
+
start_time = time.time()
|
| 118 |
+
|
| 119 |
+
for i, primjer in enumerate(primjeri):
|
| 120 |
+
|
| 121 |
+
# Ispis napretka svaki 100 primjera
|
| 122 |
+
if i > 0 and i % 100 == 0:
|
| 123 |
+
proteklo = time.time() - start_time
|
| 124 |
+
preostalo = (proteklo / i) * (len(primjeri) - i)
|
| 125 |
+
print(f" [{i}/{len(primjeri)}] "
|
| 126 |
+
f"~{preostalo:.0f}s preostalo...", end="\r")
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
# Izvuci sve značajke
|
| 130 |
+
znacajke = extract_all_features(
|
| 131 |
+
code=primjer["code"],
|
| 132 |
+
language=primjer["language"],
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Dodaj oznaku i izvor uz značajke
|
| 136 |
+
redak = {
|
| 137 |
+
"label": primjer["label"],
|
| 138 |
+
**znacajke,
|
| 139 |
+
"source": primjer["source"],
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
# Otvori CSV i piši zaglavlje samo pri prvom retku
|
| 143 |
+
if pisac is None:
|
| 144 |
+
f = open(output_path, "w", newline="", encoding="utf-8")
|
| 145 |
+
pisac = csv.DictWriter(f, fieldnames=list(redak.keys()))
|
| 146 |
+
pisac.writeheader()
|
| 147 |
+
|
| 148 |
+
pisac.writerow(redak)
|
| 149 |
+
uspjesno += 1
|
| 150 |
+
|
| 151 |
+
except Exception as e:
|
| 152 |
+
greske += 1
|
| 153 |
+
if greske <= 5: # ispiši samo prvih 5 grešaka
|
| 154 |
+
print(f"\n [UPOZORENJE] Greška na primjeru {i}: {e}")
|
| 155 |
+
|
| 156 |
+
if f:
|
| 157 |
+
f.close()
|
| 158 |
+
|
| 159 |
+
trajanje = time.time() - start_time
|
| 160 |
+
print(f"\n Gotovo za {trajanje:.1f}s")
|
| 161 |
+
print(f" Uspješno: {uspjesno} / {len(primjeri)}")
|
| 162 |
+
if greske:
|
| 163 |
+
print(f" Greške: {greske} (preskočeni)")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 167 |
+
# STATISTIKA O DATASETU
|
| 168 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 169 |
+
|
| 170 |
+
def ispisi_statistiku(csv_path: str) -> None:
|
| 171 |
+
"""
|
| 172 |
+
Učita gotovi CSV i ispiše osnovnu statistiku:
|
| 173 |
+
broj primjera, omjer klasa, raspodjela po sourceu.
|
| 174 |
+
"""
|
| 175 |
+
if not os.path.exists(csv_path):
|
| 176 |
+
print(" CSV ne postoji, nema statistike.")
|
| 177 |
+
return
|
| 178 |
+
|
| 179 |
+
redovi = []
|
| 180 |
+
with open(csv_path, "r", encoding="utf-8") as f:
|
| 181 |
+
reader = csv.DictReader(f)
|
| 182 |
+
redovi = list(reader)
|
| 183 |
+
|
| 184 |
+
if not redovi:
|
| 185 |
+
print(" CSV je prazan.")
|
| 186 |
+
return
|
| 187 |
+
|
| 188 |
+
ukupno = len(redovi)
|
| 189 |
+
human = sum(1 for r in redovi if r["label"] == "0")
|
| 190 |
+
ai = sum(1 for r in redovi if r["label"] == "1")
|
| 191 |
+
num_feats = len(redovi[0]) - 2 # -2 za label i source
|
| 192 |
+
|
| 193 |
+
# Statistika po sourceu
|
| 194 |
+
from collections import Counter
|
| 195 |
+
sources = Counter(r["source"] for r in redovi)
|
| 196 |
+
|
| 197 |
+
print(f"\n{'═' * 50}")
|
| 198 |
+
print(f" STATISTIKA DATASETA")
|
| 199 |
+
print(f"{'═' * 50}")
|
| 200 |
+
print(f" Ukupno primjera: {ukupno}")
|
| 201 |
+
print(f" Human (label=0): {human} ({100*human/ukupno:.1f}%)")
|
| 202 |
+
print(f" AI (label=1): {ai} ({100*ai/ukupno:.1f}%)")
|
| 203 |
+
print(f" Broj značajki: {num_feats}")
|
| 204 |
+
print(f"\n Po datasetu:")
|
| 205 |
+
for source, count in sources.most_common():
|
| 206 |
+
print(f" {source:<30} {count}")
|
| 207 |
+
print(f"\n Spremljeno u: {csv_path}")
|
| 208 |
+
print(f"{'═' * 50}\n")
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 212 |
+
# GLAVNI PROGRAM
|
| 213 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 214 |
+
|
| 215 |
+
def main():
|
| 216 |
+
print("=" * 50)
|
| 217 |
+
print(" Preuzimanje dataseta i izvlačenje značajki")
|
| 218 |
+
print("=" * 50)
|
| 219 |
+
|
| 220 |
+
print("\nKoji dataset želiš preuzeti?")
|
| 221 |
+
print(" 1 — AIGCodeSet (7.583 Python primjera)")
|
| 222 |
+
print(" 2 — ai-code-detection (Rosetta Code + CodeNet)")
|
| 223 |
+
print(" 3 — Oba spojena (preporučeno)")
|
| 224 |
+
|
| 225 |
+
odabir = input("\nOdabir (1/2/3): ").strip()
|
| 226 |
+
|
| 227 |
+
# Brzi test mod
|
| 228 |
+
print("\nBrzi test mod? (uzima samo 200 primjera, brže za provjeru)")
|
| 229 |
+
test_mod = input("(d/n, Enter za ne): ").strip().lower()
|
| 230 |
+
max_s = 200 if test_mod in ("d", "da", "y", "yes") else MAX_SAMPLES_PER_DATASET
|
| 231 |
+
|
| 232 |
+
# Skidanje odabranog dataseta
|
| 233 |
+
print()
|
| 234 |
+
svi_primjeri = []
|
| 235 |
+
|
| 236 |
+
if odabir in ("1", "3"):
|
| 237 |
+
svi_primjeri += parse_aigcodeset(max_samples=max_s)
|
| 238 |
+
|
| 239 |
+
if odabir in ("2", "3"):
|
| 240 |
+
svi_primjeri += parse_ai_code_detection(max_samples=max_s)
|
| 241 |
+
|
| 242 |
+
if not svi_primjeri:
|
| 243 |
+
print("\n Nema primjera za procesiranje. Provjeri internetsku vezu.")
|
| 244 |
+
return
|
| 245 |
+
|
| 246 |
+
human_ukupno = sum(1 for p in svi_primjeri if p["label"] == 0)
|
| 247 |
+
ai_ukupno = sum(1 for p in svi_primjeri if p["label"] == 1)
|
| 248 |
+
print(f"\n Ukupno primjera za procesiranje: {len(svi_primjeri)}"
|
| 249 |
+
f" ({human_ukupno} human, {ai_ukupno} AI)")
|
| 250 |
+
|
| 251 |
+
# Izvuci značajke i spremi CSV
|
| 252 |
+
procesiraj_i_spremi(svi_primjeri, OUTPUT_FILE)
|
| 253 |
+
|
| 254 |
+
# Ispiši statistiku
|
| 255 |
+
ispisi_statistiku(OUTPUT_FILE)
|
| 256 |
+
|
| 257 |
+
print(" Sljedeći korak: python classifier.py")
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
if __name__ == "__main__":
|
| 261 |
+
main()
|
feature_extraction.py
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
feature_extraction.py
|
| 3 |
+
=====================
|
| 4 |
+
Višejezično izvlačenje značajki iz programskog koda.
|
| 5 |
+
|
| 6 |
+
Podržani jezici: Python, JavaScript, TypeScript, Java, C, C++, Go, Rust, Ruby
|
| 7 |
+
|
| 8 |
+
Organizirano po načinima detekcije:
|
| 9 |
+
1. Stilska detekcija – komentari, imenovanje, formatiranje
|
| 10 |
+
2. Strukturna detekcija – AST analiza, složenost, tok kontrole
|
| 11 |
+
3. Statistička detekcija – perplexity pomoću jezičnog modela
|
| 12 |
+
|
| 13 |
+
Glavna funkcija:
|
| 14 |
+
extract_all_features(code, language=None, filename=None) -> dict
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import re
|
| 18 |
+
import math
|
| 19 |
+
from collections import Counter
|
| 20 |
+
from tree_sitter import Language, Parser
|
| 21 |
+
|
| 22 |
+
from language_config import (
|
| 23 |
+
get_config,
|
| 24 |
+
detect_language_from_code,
|
| 25 |
+
detect_language_from_extension,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 30 |
+
# POMOĆNE FUNKCIJE
|
| 31 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 32 |
+
|
| 33 |
+
def _safe_divide(a: float, b: float, default: float = 0.0) -> float:
|
| 34 |
+
"""Dijeljenje koje ne puca na nuli."""
|
| 35 |
+
return a / b if b != 0 else default
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _get_lines(code: str) -> list:
|
| 39 |
+
"""Vraća sve linije koda kao listu stringova."""
|
| 40 |
+
return code.splitlines()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _build_tree(code: str, ts_module):
|
| 44 |
+
"""
|
| 45 |
+
Parsira kod pomoću Tree-sitter i vraća stablo.
|
| 46 |
+
Koristi modul specifičan za jezik (npr. tree_sitter_python).
|
| 47 |
+
Vraća None ako parsiranje ne uspije.
|
| 48 |
+
"""
|
| 49 |
+
try:
|
| 50 |
+
lang = Language(ts_module.language())
|
| 51 |
+
parser = Parser(lang)
|
| 52 |
+
return parser.parse(code.encode("utf-8", errors="replace"))
|
| 53 |
+
except Exception:
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _walk_tree(node) -> list:
|
| 58 |
+
"""
|
| 59 |
+
Prolazi kroz cijelo Tree-sitter stablo i vraća listu svih čvorova.
|
| 60 |
+
Ekvivalent ast.walk() iz Pythonovog standardnog modula.
|
| 61 |
+
"""
|
| 62 |
+
result = [node]
|
| 63 |
+
for child in node.children:
|
| 64 |
+
result.extend(_walk_tree(child))
|
| 65 |
+
return result
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _count_node_types(all_nodes: list, type_names: list) -> int:
|
| 69 |
+
"""Broji koliko se puta pojavljuje bilo koji od zadanih tipova čvorova."""
|
| 70 |
+
if not type_names:
|
| 71 |
+
return 0
|
| 72 |
+
return sum(1 for n in all_nodes if n.type in type_names)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _get_node_depth(node, current: int = 0) -> int:
|
| 76 |
+
"""Rekurzivno računa maksimalnu dubinu stabla."""
|
| 77 |
+
if not node.children:
|
| 78 |
+
return current
|
| 79 |
+
return max(_get_node_depth(child, current + 1) for child in node.children)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 83 |
+
# NAČIN 1: STILSKA DETEKCIJA
|
| 84 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 85 |
+
|
| 86 |
+
def extract_style_features(code: str, lang_config: dict) -> dict:
|
| 87 |
+
"""
|
| 88 |
+
Izvlači stilske značajke iz koda.
|
| 89 |
+
|
| 90 |
+
Radi za sve jezike jer:
|
| 91 |
+
- komentare prepoznaje regex obrascem iz lang_config
|
| 92 |
+
(svaki jezik ima drugu sintaksu komentara)
|
| 93 |
+
- identifikatore uzima iz Tree-sitter stabla
|
| 94 |
+
(tree-sitter radi za sve podržane jezike)
|
| 95 |
+
- formatiranje gleda direktno po linijama
|
| 96 |
+
(potpuno universalno — vrijedi za sve jezike)
|
| 97 |
+
|
| 98 |
+
Parametri:
|
| 99 |
+
code (str): Izvorni kod kao string.
|
| 100 |
+
lang_config (dict): Konfiguracija jezika iz language_config.py.
|
| 101 |
+
|
| 102 |
+
Vraća:
|
| 103 |
+
dict: Stilske značajke s float/int vrijednostima.
|
| 104 |
+
"""
|
| 105 |
+
lines = _get_lines(code)
|
| 106 |
+
total_lines = len(lines) if lines else 1
|
| 107 |
+
|
| 108 |
+
# ── KOMENTARI ──────────────────────────────────────────────────────────
|
| 109 |
+
# Svaki jezik ima drugačiji simbol — Python koristi #, Java/JS koriste //
|
| 110 |
+
# Regex obrazac je definiran u language_config.py za svaki jezik.
|
| 111 |
+
|
| 112 |
+
inline_pat = lang_config["inline_comment"]
|
| 113 |
+
comment_lines = [l for l in lines if re.match(inline_pat, l)]
|
| 114 |
+
num_comment_lines = len(comment_lines)
|
| 115 |
+
|
| 116 |
+
# Prosječna duljina komentara u riječima
|
| 117 |
+
comment_words_total = sum(
|
| 118 |
+
len(re.sub(inline_pat, "", l).strip().split())
|
| 119 |
+
for l in comment_lines
|
| 120 |
+
)
|
| 121 |
+
avg_comment_length_words = _safe_divide(comment_words_total, num_comment_lines)
|
| 122 |
+
|
| 123 |
+
# Blok komentari: /* ... */ u Java/JS/C, =begin...=end u Ruby
|
| 124 |
+
num_block_comments = 0
|
| 125 |
+
if lang_config["block_comment"]:
|
| 126 |
+
start, end = lang_config["block_comment"]
|
| 127 |
+
num_block_comments = len(re.findall(
|
| 128 |
+
re.escape(start) + r"[\s\S]*?" + re.escape(end),
|
| 129 |
+
code
|
| 130 |
+
))
|
| 131 |
+
|
| 132 |
+
# Ukupan udio komentara u znakovima
|
| 133 |
+
total_comment_chars = sum(len(l) for l in comment_lines)
|
| 134 |
+
comment_to_code_ratio = _safe_divide(total_comment_chars, max(len(code), 1))
|
| 135 |
+
|
| 136 |
+
# Dokumentacijski komentari (docstring, JSDoc, Javadoc...)
|
| 137 |
+
num_docstrings = 0
|
| 138 |
+
if lang_config.get("docstring_pattern"):
|
| 139 |
+
num_docstrings = len(re.findall(lang_config["docstring_pattern"], code))
|
| 140 |
+
|
| 141 |
+
# ── IMENOVANJE (iz Tree-sitter stabla) ────────────────────────────────
|
| 142 |
+
# Tree-sitter za svaki jezik daje čvorove tipa "identifier"
|
| 143 |
+
# koji sadrže nazive varijabli, funkcija, argumenata itd.
|
| 144 |
+
|
| 145 |
+
identifier_names = []
|
| 146 |
+
function_names = []
|
| 147 |
+
|
| 148 |
+
tree = _build_tree(code, lang_config["ts_module"])
|
| 149 |
+
if tree:
|
| 150 |
+
all_nodes = _walk_tree(tree.root_node)
|
| 151 |
+
id_types = lang_config["node_types"].get("identifier", ["identifier"])
|
| 152 |
+
fn_types = lang_config["node_types"].get("function", [])
|
| 153 |
+
|
| 154 |
+
# Skupljamo sve identifikatore
|
| 155 |
+
for node in all_nodes:
|
| 156 |
+
if node.type in id_types and node.text:
|
| 157 |
+
name = node.text.decode("utf-8", errors="replace")
|
| 158 |
+
if len(name) >= 1:
|
| 159 |
+
identifier_names.append(name)
|
| 160 |
+
|
| 161 |
+
# Skupljamo nazive funkcija — tražimo "identifier" dijete
|
| 162 |
+
# unutar čvora koji označava funkciju
|
| 163 |
+
for node in all_nodes:
|
| 164 |
+
if node.type in fn_types:
|
| 165 |
+
for child in node.children:
|
| 166 |
+
if child.type in id_types and child.text:
|
| 167 |
+
fn_name = child.text.decode("utf-8", errors="replace")
|
| 168 |
+
function_names.append(fn_name)
|
| 169 |
+
break
|
| 170 |
+
|
| 171 |
+
avg_identifier_length = _safe_divide(
|
| 172 |
+
sum(len(n) for n in identifier_names), len(identifier_names)
|
| 173 |
+
)
|
| 174 |
+
avg_function_name_length = _safe_divide(
|
| 175 |
+
sum(len(n) for n in function_names), len(function_names)
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# Jednoslovna imena (i, x, n, k...) — čovječji kod ih ima više
|
| 179 |
+
single_char_count = sum(1 for n in identifier_names if len(n) == 1)
|
| 180 |
+
single_char_ratio = _safe_divide(single_char_count, len(identifier_names))
|
| 181 |
+
|
| 182 |
+
# Leksička raznolikost: visoka = raznovrsni nazivi (čovjek), niska = AI ponavlja obrasce
|
| 183 |
+
lexical_diversity = _safe_divide(
|
| 184 |
+
len(set(identifier_names)), len(identifier_names)
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Konvencije imenovanja
|
| 188 |
+
def is_snake_case(n):
|
| 189 |
+
return "_" in n and n == n.lower() and not n.startswith("_")
|
| 190 |
+
|
| 191 |
+
def is_camel_case(n):
|
| 192 |
+
return (len(n) > 1 and not n.startswith("_")
|
| 193 |
+
and n[0].islower() and any(c.isupper() for c in n)
|
| 194 |
+
and "_" not in n)
|
| 195 |
+
|
| 196 |
+
def is_pascal_case(n):
|
| 197 |
+
return (len(n) > 1 and n[0].isupper()
|
| 198 |
+
and any(c.islower() for c in n) and "_" not in n)
|
| 199 |
+
|
| 200 |
+
total_ids = max(len(identifier_names), 1)
|
| 201 |
+
snake_count = sum(1 for n in identifier_names if is_snake_case(n))
|
| 202 |
+
camel_count = sum(1 for n in identifier_names if is_camel_case(n))
|
| 203 |
+
pascal_count = sum(1 for n in identifier_names if is_pascal_case(n))
|
| 204 |
+
|
| 205 |
+
snake_ratio = _safe_divide(snake_count, total_ids)
|
| 206 |
+
camel_ratio = _safe_divide(camel_count, total_ids)
|
| 207 |
+
|
| 208 |
+
# Konzistentnost imenovanja: 1.0 = svi identifikatori u istom stilu (tipično AI)
|
| 209 |
+
naming_consistency = _safe_divide(
|
| 210 |
+
max(snake_count, camel_count, pascal_count), total_ids
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# ── FORMATIRANJE (potpuno universalno za sve jezike) ───────────────────
|
| 214 |
+
|
| 215 |
+
non_empty_lines = [l for l in lines if l.strip()]
|
| 216 |
+
empty_line_ratio = _safe_divide(total_lines - len(non_empty_lines), total_lines)
|
| 217 |
+
|
| 218 |
+
line_lengths = [len(l) for l in non_empty_lines] if non_empty_lines else [0]
|
| 219 |
+
avg_line_length = _safe_divide(sum(line_lengths), len(line_lengths))
|
| 220 |
+
max_line_length = max(line_lengths) if line_lengths else 0
|
| 221 |
+
|
| 222 |
+
# Tabovi vs razmaci za uvlačenje
|
| 223 |
+
tab_lines = sum(1 for l in lines if l.startswith("\t"))
|
| 224 |
+
uses_tabs = int(tab_lines > len(lines) * 0.1)
|
| 225 |
+
|
| 226 |
+
# Trailing whitespace — razmaci na kraju linije
|
| 227 |
+
trailing_ws = sum(1 for l in lines if l != l.rstrip())
|
| 228 |
+
trailing_ws_ratio = _safe_divide(trailing_ws, total_lines)
|
| 229 |
+
|
| 230 |
+
# Konzistentnost razmaka oko operatora (= == != < >...)
|
| 231 |
+
with_space = len(re.findall(r"\s[=!<>]=?\s", code))
|
| 232 |
+
without_space = len(re.findall(r"[^\s=!<>][=!<>]=[^\s=]", code))
|
| 233 |
+
operator_consistency = _safe_divide(
|
| 234 |
+
max(with_space, without_space), with_space + without_space
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
return {
|
| 238 |
+
# Komentari
|
| 239 |
+
"num_comment_lines": num_comment_lines,
|
| 240 |
+
"comment_ratio": _safe_divide(num_comment_lines, total_lines),
|
| 241 |
+
"avg_comment_length_words": avg_comment_length_words,
|
| 242 |
+
"comment_to_code_ratio": comment_to_code_ratio,
|
| 243 |
+
"num_block_comments": num_block_comments,
|
| 244 |
+
"num_docstrings": num_docstrings,
|
| 245 |
+
# Imenovanje
|
| 246 |
+
"avg_identifier_length": avg_identifier_length,
|
| 247 |
+
"avg_function_name_length": avg_function_name_length,
|
| 248 |
+
"single_char_name_ratio": single_char_ratio,
|
| 249 |
+
"lexical_diversity": lexical_diversity,
|
| 250 |
+
"snake_case_ratio": snake_ratio,
|
| 251 |
+
"camel_case_ratio": camel_ratio,
|
| 252 |
+
"naming_consistency": naming_consistency,
|
| 253 |
+
# Formatiranje
|
| 254 |
+
"total_lines": total_lines,
|
| 255 |
+
"empty_line_ratio": empty_line_ratio,
|
| 256 |
+
"avg_line_length": avg_line_length,
|
| 257 |
+
"max_line_length": max_line_length,
|
| 258 |
+
"uses_tabs": uses_tabs,
|
| 259 |
+
"trailing_whitespace_ratio": trailing_ws_ratio,
|
| 260 |
+
"operator_spacing_consistency": operator_consistency,
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 265 |
+
# NAČIN 2: STRUKTURNA DETEKCIJA
|
| 266 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 267 |
+
|
| 268 |
+
def extract_structural_features(code: str, lang_config: dict) -> dict:
|
| 269 |
+
"""
|
| 270 |
+
Izvlači strukturne značajke iz koda pomoću Tree-sitter AST analize.
|
| 271 |
+
|
| 272 |
+
Tree-sitter radi za sve podržane jezike — jedina razlika su nazivi
|
| 273 |
+
čvorova (npr. "function_definition" u Pythonu vs "method_declaration"
|
| 274 |
+
u Javi), a to je riješeno kroz lang_config["node_types"].
|
| 275 |
+
|
| 276 |
+
Parametri:
|
| 277 |
+
code (str): Izvorni kod kao string.
|
| 278 |
+
lang_config (dict): Konfiguracija jezika iz language_config.py.
|
| 279 |
+
|
| 280 |
+
Vraća:
|
| 281 |
+
dict: Strukturne značajke, ili rječnik nula ako parsiranje ne uspije.
|
| 282 |
+
"""
|
| 283 |
+
empty_result = {k: 0 for k in [
|
| 284 |
+
"ast_depth", "ast_node_count", "unique_node_type_ratio",
|
| 285 |
+
"num_functions", "avg_function_length", "max_function_length",
|
| 286 |
+
"avg_args_per_function", "num_classes", "num_imports",
|
| 287 |
+
"num_if_statements", "num_for_loops", "num_while_loops",
|
| 288 |
+
"num_try_blocks", "num_lambdas",
|
| 289 |
+
"max_nesting_depth", "avg_nesting_depth",
|
| 290 |
+
"cyclomatic_complexity_approx",
|
| 291 |
+
]}
|
| 292 |
+
|
| 293 |
+
tree = _build_tree(code, lang_config["ts_module"])
|
| 294 |
+
if tree is None:
|
| 295 |
+
return empty_result
|
| 296 |
+
|
| 297 |
+
root = tree.root_node
|
| 298 |
+
all_nodes = _walk_tree(root)
|
| 299 |
+
nt = lang_config["node_types"]
|
| 300 |
+
|
| 301 |
+
# ── AST STABLO ─────────────────────────────────────────────────────────
|
| 302 |
+
|
| 303 |
+
ast_depth = _get_node_depth(root)
|
| 304 |
+
ast_node_count = len(all_nodes)
|
| 305 |
+
node_type_counts = Counter(n.type for n in all_nodes)
|
| 306 |
+
unique_node_type_ratio = _safe_divide(len(node_type_counts), ast_node_count)
|
| 307 |
+
|
| 308 |
+
# ── FUNKCIJE ───────────────────────────────────────────────────────────
|
| 309 |
+
|
| 310 |
+
function_nodes = [n for n in all_nodes if n.type in nt.get("function", [])]
|
| 311 |
+
num_functions = len(function_nodes)
|
| 312 |
+
|
| 313 |
+
# Duljina svake funkcije u linijama koda
|
| 314 |
+
function_lengths = [
|
| 315 |
+
fn.end_point[0] - fn.start_point[0] + 1
|
| 316 |
+
for fn in function_nodes
|
| 317 |
+
]
|
| 318 |
+
avg_function_length = _safe_divide(sum(function_lengths), len(function_lengths))
|
| 319 |
+
max_function_length = max(function_lengths) if function_lengths else 0
|
| 320 |
+
|
| 321 |
+
# Broj parametara po funkciji
|
| 322 |
+
args_counts = []
|
| 323 |
+
for fn in function_nodes:
|
| 324 |
+
for child in fn.children:
|
| 325 |
+
if child.type in ("parameters", "formal_parameters",
|
| 326 |
+
"parameter_list", "argument_list"):
|
| 327 |
+
params = [c for c in child.children
|
| 328 |
+
if c.type not in ("(", ")", ",", "self")]
|
| 329 |
+
args_counts.append(len(params))
|
| 330 |
+
break
|
| 331 |
+
avg_args_per_function = _safe_divide(sum(args_counts), len(args_counts))
|
| 332 |
+
|
| 333 |
+
# ── KLASE I IMPORTI ────────────────────────────────────────────────────
|
| 334 |
+
|
| 335 |
+
num_classes = _count_node_types(all_nodes, nt.get("class", []))
|
| 336 |
+
num_imports = _count_node_types(all_nodes, nt.get("import", []))
|
| 337 |
+
|
| 338 |
+
# ── TOK KONTROLE ───────────────────────────────────────────────────────
|
| 339 |
+
|
| 340 |
+
num_if = _count_node_types(all_nodes, nt.get("if", []))
|
| 341 |
+
num_for = _count_node_types(all_nodes, nt.get("for", []))
|
| 342 |
+
num_while = _count_node_types(all_nodes, nt.get("while", []))
|
| 343 |
+
num_try = _count_node_types(all_nodes, nt.get("try", []))
|
| 344 |
+
num_lambdas = _count_node_types(all_nodes, nt.get("lambda", []))
|
| 345 |
+
|
| 346 |
+
# ── DUBINA UGNIJEŽĐENOSTI ──────────────────────────────────────────────
|
| 347 |
+
|
| 348 |
+
nesting_types = set(
|
| 349 |
+
nt.get("if", []) + nt.get("for", []) +
|
| 350 |
+
nt.get("while", []) + nt.get("function", [])
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
def collect_depths(node, depth=0):
|
| 354 |
+
depths = []
|
| 355 |
+
for child in node.children:
|
| 356 |
+
if child.type in nesting_types:
|
| 357 |
+
depths.append(depth + 1)
|
| 358 |
+
depths.extend(collect_depths(child, depth + 1))
|
| 359 |
+
else:
|
| 360 |
+
depths.extend(collect_depths(child, depth))
|
| 361 |
+
return depths
|
| 362 |
+
|
| 363 |
+
nesting_depths = collect_depths(root)
|
| 364 |
+
max_nesting_depth = max(nesting_depths) if nesting_depths else 0
|
| 365 |
+
avg_nesting_depth = _safe_divide(sum(nesting_depths), len(nesting_depths))
|
| 366 |
+
|
| 367 |
+
# ── APROKSIMACIJA CIKLOMATSKE SLOŽENOSTI ──────────────────────────────
|
| 368 |
+
# CC ≈ 1 + broj grananja, dijeljeno brojem funkcija
|
| 369 |
+
# Standardna aproksimacija koja radi za sve jezike.
|
| 370 |
+
|
| 371 |
+
total_branches = 1 + num_if + num_for + num_while + num_try
|
| 372 |
+
cyclomatic_approx = (
|
| 373 |
+
_safe_divide(total_branches, num_functions)
|
| 374 |
+
if num_functions > 0
|
| 375 |
+
else float(total_branches)
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
# ── NORMALIZACIJA PO VELIČINI KODA ────────────────────────────────────
|
| 379 |
+
# Apsolutni brojevi (num_if, num_for...) ovise o veličini koda.
|
| 380 |
+
# Dijeljenjem s brojem nepraznih linija dobivamo gustoću koja je
|
| 381 |
+
# usporediva između kratkih i dugih kodova.
|
| 382 |
+
# Npr. 3 if-a u 10 linija (0.30) vs 3 if-a u 100 linija (0.03)
|
| 383 |
+
# — apsolutni broj je isti, ali gustoća govori pravu priču.
|
| 384 |
+
|
| 385 |
+
lines_all = _get_lines(code)
|
| 386 |
+
non_empty = max(sum(1 for l in lines_all if l.strip()), 1)
|
| 387 |
+
|
| 388 |
+
if_density = _safe_divide(num_if, non_empty)
|
| 389 |
+
for_density = _safe_divide(num_for, non_empty)
|
| 390 |
+
while_density = _safe_divide(num_while, non_empty)
|
| 391 |
+
try_density = _safe_divide(num_try, non_empty)
|
| 392 |
+
function_density = _safe_divide(num_functions, non_empty)
|
| 393 |
+
class_density = _safe_divide(num_classes, non_empty)
|
| 394 |
+
import_density = _safe_divide(num_imports, non_empty)
|
| 395 |
+
lambda_density = _safe_divide(num_lambdas, non_empty)
|
| 396 |
+
|
| 397 |
+
# Broj AST čvorova po liniji — AI kod ima predvidljive strukture
|
| 398 |
+
ast_nodes_per_line = _safe_divide(ast_node_count, non_empty)
|
| 399 |
+
|
| 400 |
+
# ── ENTROPIJA KODA ────────────────────────────────────────────────────
|
| 401 |
+
# Entropija mjeri raznolikost i nepredvidljivost koda.
|
| 402 |
+
# Visoka entropija = raznolik, nepredvidljiv kod = vjerojatno čovjek
|
| 403 |
+
# Niska entropija = ponavljajući, predvidljiv kod = možda AI
|
| 404 |
+
#
|
| 405 |
+
# Formula: H = -sum(p * log2(p)) za svaki jedinstveni element
|
| 406 |
+
|
| 407 |
+
import math as _math
|
| 408 |
+
from collections import Counter as _Counter
|
| 409 |
+
|
| 410 |
+
# Entropija znakova — distribucija pojedinih znakova u kodu
|
| 411 |
+
char_counts = _Counter(code)
|
| 412 |
+
total_chars = len(code) if code else 1
|
| 413 |
+
char_entropy = -sum(
|
| 414 |
+
(c / total_chars) * _math.log2(c / total_chars)
|
| 415 |
+
for c in char_counts.values()
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
# Entropija tokena — raznolikost na razini imenskih jedinica i simbola
|
| 419 |
+
# Bolji signal od entropije znakova jer gleda smislene jezične jedinice
|
| 420 |
+
tokens = re.findall(r"[a-zA-Z_]\w*|[0-9]+|[^\w\s]", code)
|
| 421 |
+
token_counts = _Counter(tokens)
|
| 422 |
+
total_tokens = len(tokens) if tokens else 1
|
| 423 |
+
token_entropy = -sum(
|
| 424 |
+
(c / total_tokens) * _math.log2(c / total_tokens)
|
| 425 |
+
for c in token_counts.values()
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
return {
|
| 429 |
+
# AST metrike (neovisne o veličini)
|
| 430 |
+
"ast_depth": ast_depth,
|
| 431 |
+
"unique_node_type_ratio": unique_node_type_ratio,
|
| 432 |
+
"ast_nodes_per_line": ast_nodes_per_line,
|
| 433 |
+
# Funkcije (prosjeci su već neovisni o veličini)
|
| 434 |
+
"avg_function_length": avg_function_length,
|
| 435 |
+
"max_function_length": max_function_length,
|
| 436 |
+
"avg_args_per_function": avg_args_per_function,
|
| 437 |
+
# Gustoće — normalizirane po nepraznim linijama koda
|
| 438 |
+
"function_density": function_density,
|
| 439 |
+
"class_density": class_density,
|
| 440 |
+
"import_density": import_density,
|
| 441 |
+
"if_density": if_density,
|
| 442 |
+
"for_density": for_density,
|
| 443 |
+
"while_density": while_density,
|
| 444 |
+
"try_density": try_density,
|
| 445 |
+
"lambda_density": lambda_density,
|
| 446 |
+
# Ugniježđenost i složenost (već neovisni o veličini)
|
| 447 |
+
"max_nesting_depth": max_nesting_depth,
|
| 448 |
+
"avg_nesting_depth": avg_nesting_depth,
|
| 449 |
+
"cyclomatic_complexity_approx": cyclomatic_approx,
|
| 450 |
+
# Entropija
|
| 451 |
+
"char_entropy": char_entropy,
|
| 452 |
+
"token_entropy": token_entropy,
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 457 |
+
# NAČIN 3: STATISTIČKA DETEKCIJA (Perplexity)
|
| 458 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 459 |
+
|
| 460 |
+
def extract_statistical_features(code: str, model=None, tokenizer=None) -> dict:
|
| 461 |
+
"""
|
| 462 |
+
Računa perplexity koda pomoću jezičnog modela.
|
| 463 |
+
|
| 464 |
+
Niski perplexity → model je "očekivao" kod → vjerojatno AI.
|
| 465 |
+
Visoki perplexity → model je "iznenađen" → vjerojatno čovjek.
|
| 466 |
+
|
| 467 |
+
Metoda je jezično-agnostična — isti model prima kod u bilo kojem jeziku.
|
| 468 |
+
Ako model nije proslijeđen (None), vraća -1 i ostatak pipeline-a nastavlja
|
| 469 |
+
normalno bez statističke značajke.
|
| 470 |
+
"""
|
| 471 |
+
if model is None or tokenizer is None:
|
| 472 |
+
return {"perplexity": -1.0, "model_available": 0}
|
| 473 |
+
|
| 474 |
+
try:
|
| 475 |
+
import torch
|
| 476 |
+
|
| 477 |
+
inputs = tokenizer(
|
| 478 |
+
code, return_tensors="pt", truncation=True, max_length=512
|
| 479 |
+
)
|
| 480 |
+
with torch.no_grad():
|
| 481 |
+
outputs = model(inputs["input_ids"], labels=inputs["input_ids"])
|
| 482 |
+
loss = outputs.loss
|
| 483 |
+
|
| 484 |
+
perplexity = math.exp(loss.item())
|
| 485 |
+
|
| 486 |
+
except Exception as e:
|
| 487 |
+
print(f" [UPOZORENJE] Perplexity nije izračunat: {e}")
|
| 488 |
+
perplexity = -1.0
|
| 489 |
+
|
| 490 |
+
return {"perplexity": perplexity, "model_available": 1}
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 494 |
+
# KOMBINIRANA FUNKCIJA — ulazna točka
|
| 495 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 496 |
+
|
| 497 |
+
def extract_all_features(
|
| 498 |
+
code: str,
|
| 499 |
+
language=None,
|
| 500 |
+
filename=None,
|
| 501 |
+
model=None,
|
| 502 |
+
tokenizer=None,
|
| 503 |
+
) -> dict:
|
| 504 |
+
"""
|
| 505 |
+
Izvlači SVE značajke iz koda u jednom pozivu.
|
| 506 |
+
|
| 507 |
+
Jezik se određuje ovim redoslijedom:
|
| 508 |
+
1. Argument language (ako je zadan)
|
| 509 |
+
2. Nastavak datoteke iz filename (ako je zadan)
|
| 510 |
+
3. Heuristike iz samog koda (automatska detekcija)
|
| 511 |
+
|
| 512 |
+
Parametri:
|
| 513 |
+
code (str): Izvorni kod kao string.
|
| 514 |
+
language (str|None): Naziv jezika (npr. "python", "java").
|
| 515 |
+
filename (str|None): Ime datoteke (npr. "main.py").
|
| 516 |
+
model: (opcionalno) HuggingFace model za perplexity.
|
| 517 |
+
tokenizer: (opcionalno) HuggingFace tokenizator.
|
| 518 |
+
|
| 519 |
+
Vraća:
|
| 520 |
+
dict: Sve značajke + ključ "detected_language".
|
| 521 |
+
"""
|
| 522 |
+
# Određivanje jezika
|
| 523 |
+
if language is not None:
|
| 524 |
+
detected_lang = language.lower()
|
| 525 |
+
elif filename is not None:
|
| 526 |
+
detected_lang = (detect_language_from_extension(filename)
|
| 527 |
+
or detect_language_from_code(code))
|
| 528 |
+
else:
|
| 529 |
+
detected_lang = detect_language_from_code(code)
|
| 530 |
+
|
| 531 |
+
lang_config = get_config(detected_lang)
|
| 532 |
+
|
| 533 |
+
# Izvlačenje značajki po metodama
|
| 534 |
+
style_feats = extract_style_features(code, lang_config)
|
| 535 |
+
structural_feats = extract_structural_features(code, lang_config)
|
| 536 |
+
statistical_feats = extract_statistical_features(code, model, tokenizer)
|
| 537 |
+
|
| 538 |
+
return {
|
| 539 |
+
"detected_language": detected_lang,
|
| 540 |
+
**style_feats,
|
| 541 |
+
**structural_feats,
|
| 542 |
+
**statistical_feats,
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 547 |
+
# BRZI TEST — pokreni: python feature_extraction.py
|
| 548 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 549 |
+
|
| 550 |
+
if __name__ == "__main__":
|
| 551 |
+
|
| 552 |
+
test_cases = {
|
| 553 |
+
"Python (AI)": ("python", '''
|
| 554 |
+
def calculate_average(numbers: list) -> float:
|
| 555 |
+
"""Calculate the arithmetic mean of a list of numbers."""
|
| 556 |
+
if not numbers:
|
| 557 |
+
raise ValueError("Cannot calculate average of an empty list.")
|
| 558 |
+
total_sum = sum(numbers)
|
| 559 |
+
count = len(numbers)
|
| 560 |
+
return total_sum / count
|
| 561 |
+
'''),
|
| 562 |
+
"Python (Human)": ("python", '''
|
| 563 |
+
def avg(nums):
|
| 564 |
+
# quick avg
|
| 565 |
+
return sum(nums) / len(nums)
|
| 566 |
+
'''),
|
| 567 |
+
"JavaScript (AI)": ("javascript", '''
|
| 568 |
+
/**
|
| 569 |
+
* Calculates the average of an array of numbers.
|
| 570 |
+
*/
|
| 571 |
+
function calculateAverage(numbers) {
|
| 572 |
+
if (!numbers || numbers.length === 0) {
|
| 573 |
+
throw new Error("Cannot calculate average of an empty array.");
|
| 574 |
+
}
|
| 575 |
+
const totalSum = numbers.reduce((acc, val) => acc + val, 0);
|
| 576 |
+
return totalSum / numbers.length;
|
| 577 |
+
}
|
| 578 |
+
'''),
|
| 579 |
+
"Java (AI)": ("java", '''
|
| 580 |
+
/**
|
| 581 |
+
* Calculates the average of an integer array.
|
| 582 |
+
*/
|
| 583 |
+
public class Calculator {
|
| 584 |
+
public static double calculateAverage(int[] numbers) {
|
| 585 |
+
if (numbers == null || numbers.length == 0) {
|
| 586 |
+
throw new IllegalArgumentException("Array must not be empty.");
|
| 587 |
+
}
|
| 588 |
+
int totalSum = 0;
|
| 589 |
+
for (int currentNumber : numbers) {
|
| 590 |
+
totalSum += currentNumber;
|
| 591 |
+
}
|
| 592 |
+
return (double) totalSum / numbers.length;
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
'''),
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
KEY_FEATURES = [
|
| 599 |
+
("Prepoznat jezik", "detected_language"),
|
| 600 |
+
("Omjer komentara", "comment_ratio"),
|
| 601 |
+
("Broj docstringova", "num_docstrings"),
|
| 602 |
+
("Prosj. duljina identif.", "avg_identifier_length"),
|
| 603 |
+
("Prosj. duljina fun. naziva", "avg_function_name_length"),
|
| 604 |
+
("Jednoslovna imena", "single_char_name_ratio"),
|
| 605 |
+
("Konzistentnost imenovanja", "naming_consistency"),
|
| 606 |
+
("Broj funkcija", "num_functions"),
|
| 607 |
+
("Prosj. duljina funkcije", "avg_function_length"),
|
| 608 |
+
("Max ugniježđenost", "max_nesting_depth"),
|
| 609 |
+
("Aproks. složenost (CC)", "cyclomatic_complexity_approx"),
|
| 610 |
+
("Perplexity", "perplexity"),
|
| 611 |
+
]
|
| 612 |
+
|
| 613 |
+
for label, (lang, code) in test_cases.items():
|
| 614 |
+
print(f"\n{'═' * 55}")
|
| 615 |
+
print(f" {label}")
|
| 616 |
+
print(f"{'═' * 55}")
|
| 617 |
+
features = extract_all_features(code, language=lang)
|
| 618 |
+
for display_name, key in KEY_FEATURES:
|
| 619 |
+
val = features.get(key, "N/A")
|
| 620 |
+
if isinstance(val, float):
|
| 621 |
+
print(f" {display_name:<32} {val:.4f}")
|
| 622 |
+
else:
|
| 623 |
+
print(f" {display_name:<32} {val}")
|
| 624 |
+
|
| 625 |
+
print(f"\n Ukupno značajki: {len(features)}")
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
| 9 |
+
|
| 10 |
+
## React Compiler
|
| 11 |
+
|
| 12 |
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
+
|
| 14 |
+
## Expanding the ESLint configuration
|
| 15 |
+
|
| 16 |
+
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 6 |
+
|
| 7 |
+
export default defineConfig([
|
| 8 |
+
globalIgnores(['dist']),
|
| 9 |
+
{
|
| 10 |
+
files: ['**/*.{js,jsx}'],
|
| 11 |
+
extends: [
|
| 12 |
+
js.configs.recommended,
|
| 13 |
+
reactHooks.configs.flat.recommended,
|
| 14 |
+
reactRefresh.configs.vite,
|
| 15 |
+
],
|
| 16 |
+
languageOptions: {
|
| 17 |
+
globals: globals.browser,
|
| 18 |
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
])
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>CodeSentinel</title>
|
| 7 |
+
<link rel="icon" type="image/svg+xml" href="/shield.svg" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
@@ -0,0 +1,1677 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "codesentinel-frontend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "codesentinel-frontend",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"react": "^18.3.1",
|
| 12 |
+
"react-dom": "^18.3.1"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 16 |
+
"vite": "^5.4.2"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"node_modules/@babel/code-frame": {
|
| 20 |
+
"version": "7.29.0",
|
| 21 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 22 |
+
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 23 |
+
"dev": true,
|
| 24 |
+
"license": "MIT",
|
| 25 |
+
"dependencies": {
|
| 26 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 27 |
+
"js-tokens": "^4.0.0",
|
| 28 |
+
"picocolors": "^1.1.1"
|
| 29 |
+
},
|
| 30 |
+
"engines": {
|
| 31 |
+
"node": ">=6.9.0"
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"node_modules/@babel/compat-data": {
|
| 35 |
+
"version": "7.29.3",
|
| 36 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
|
| 37 |
+
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
|
| 38 |
+
"dev": true,
|
| 39 |
+
"license": "MIT",
|
| 40 |
+
"engines": {
|
| 41 |
+
"node": ">=6.9.0"
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"node_modules/@babel/core": {
|
| 45 |
+
"version": "7.29.0",
|
| 46 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
| 47 |
+
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
| 48 |
+
"dev": true,
|
| 49 |
+
"license": "MIT",
|
| 50 |
+
"dependencies": {
|
| 51 |
+
"@babel/code-frame": "^7.29.0",
|
| 52 |
+
"@babel/generator": "^7.29.0",
|
| 53 |
+
"@babel/helper-compilation-targets": "^7.28.6",
|
| 54 |
+
"@babel/helper-module-transforms": "^7.28.6",
|
| 55 |
+
"@babel/helpers": "^7.28.6",
|
| 56 |
+
"@babel/parser": "^7.29.0",
|
| 57 |
+
"@babel/template": "^7.28.6",
|
| 58 |
+
"@babel/traverse": "^7.29.0",
|
| 59 |
+
"@babel/types": "^7.29.0",
|
| 60 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 61 |
+
"convert-source-map": "^2.0.0",
|
| 62 |
+
"debug": "^4.1.0",
|
| 63 |
+
"gensync": "^1.0.0-beta.2",
|
| 64 |
+
"json5": "^2.2.3",
|
| 65 |
+
"semver": "^6.3.1"
|
| 66 |
+
},
|
| 67 |
+
"engines": {
|
| 68 |
+
"node": ">=6.9.0"
|
| 69 |
+
},
|
| 70 |
+
"funding": {
|
| 71 |
+
"type": "opencollective",
|
| 72 |
+
"url": "https://opencollective.com/babel"
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"node_modules/@babel/generator": {
|
| 76 |
+
"version": "7.29.1",
|
| 77 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 78 |
+
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 79 |
+
"dev": true,
|
| 80 |
+
"license": "MIT",
|
| 81 |
+
"dependencies": {
|
| 82 |
+
"@babel/parser": "^7.29.0",
|
| 83 |
+
"@babel/types": "^7.29.0",
|
| 84 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 85 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 86 |
+
"jsesc": "^3.0.2"
|
| 87 |
+
},
|
| 88 |
+
"engines": {
|
| 89 |
+
"node": ">=6.9.0"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 93 |
+
"version": "7.28.6",
|
| 94 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
| 95 |
+
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
| 96 |
+
"dev": true,
|
| 97 |
+
"license": "MIT",
|
| 98 |
+
"dependencies": {
|
| 99 |
+
"@babel/compat-data": "^7.28.6",
|
| 100 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 101 |
+
"browserslist": "^4.24.0",
|
| 102 |
+
"lru-cache": "^5.1.1",
|
| 103 |
+
"semver": "^6.3.1"
|
| 104 |
+
},
|
| 105 |
+
"engines": {
|
| 106 |
+
"node": ">=6.9.0"
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"node_modules/@babel/helper-globals": {
|
| 110 |
+
"version": "7.28.0",
|
| 111 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 112 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 113 |
+
"dev": true,
|
| 114 |
+
"license": "MIT",
|
| 115 |
+
"engines": {
|
| 116 |
+
"node": ">=6.9.0"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"node_modules/@babel/helper-module-imports": {
|
| 120 |
+
"version": "7.28.6",
|
| 121 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 122 |
+
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 123 |
+
"dev": true,
|
| 124 |
+
"license": "MIT",
|
| 125 |
+
"dependencies": {
|
| 126 |
+
"@babel/traverse": "^7.28.6",
|
| 127 |
+
"@babel/types": "^7.28.6"
|
| 128 |
+
},
|
| 129 |
+
"engines": {
|
| 130 |
+
"node": ">=6.9.0"
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 134 |
+
"version": "7.28.6",
|
| 135 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
| 136 |
+
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
| 137 |
+
"dev": true,
|
| 138 |
+
"license": "MIT",
|
| 139 |
+
"dependencies": {
|
| 140 |
+
"@babel/helper-module-imports": "^7.28.6",
|
| 141 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 142 |
+
"@babel/traverse": "^7.28.6"
|
| 143 |
+
},
|
| 144 |
+
"engines": {
|
| 145 |
+
"node": ">=6.9.0"
|
| 146 |
+
},
|
| 147 |
+
"peerDependencies": {
|
| 148 |
+
"@babel/core": "^7.0.0"
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 152 |
+
"version": "7.28.6",
|
| 153 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
| 154 |
+
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
| 155 |
+
"dev": true,
|
| 156 |
+
"license": "MIT",
|
| 157 |
+
"engines": {
|
| 158 |
+
"node": ">=6.9.0"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"node_modules/@babel/helper-string-parser": {
|
| 162 |
+
"version": "7.27.1",
|
| 163 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 164 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 165 |
+
"dev": true,
|
| 166 |
+
"license": "MIT",
|
| 167 |
+
"engines": {
|
| 168 |
+
"node": ">=6.9.0"
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 172 |
+
"version": "7.28.5",
|
| 173 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 174 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 175 |
+
"dev": true,
|
| 176 |
+
"license": "MIT",
|
| 177 |
+
"engines": {
|
| 178 |
+
"node": ">=6.9.0"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"node_modules/@babel/helper-validator-option": {
|
| 182 |
+
"version": "7.27.1",
|
| 183 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 184 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 185 |
+
"dev": true,
|
| 186 |
+
"license": "MIT",
|
| 187 |
+
"engines": {
|
| 188 |
+
"node": ">=6.9.0"
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
"node_modules/@babel/helpers": {
|
| 192 |
+
"version": "7.29.2",
|
| 193 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
|
| 194 |
+
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
|
| 195 |
+
"dev": true,
|
| 196 |
+
"license": "MIT",
|
| 197 |
+
"dependencies": {
|
| 198 |
+
"@babel/template": "^7.28.6",
|
| 199 |
+
"@babel/types": "^7.29.0"
|
| 200 |
+
},
|
| 201 |
+
"engines": {
|
| 202 |
+
"node": ">=6.9.0"
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"node_modules/@babel/parser": {
|
| 206 |
+
"version": "7.29.3",
|
| 207 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
|
| 208 |
+
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
|
| 209 |
+
"dev": true,
|
| 210 |
+
"license": "MIT",
|
| 211 |
+
"dependencies": {
|
| 212 |
+
"@babel/types": "^7.29.0"
|
| 213 |
+
},
|
| 214 |
+
"bin": {
|
| 215 |
+
"parser": "bin/babel-parser.js"
|
| 216 |
+
},
|
| 217 |
+
"engines": {
|
| 218 |
+
"node": ">=6.0.0"
|
| 219 |
+
}
|
| 220 |
+
},
|
| 221 |
+
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
| 222 |
+
"version": "7.27.1",
|
| 223 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
| 224 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 225 |
+
"dev": true,
|
| 226 |
+
"license": "MIT",
|
| 227 |
+
"dependencies": {
|
| 228 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 229 |
+
},
|
| 230 |
+
"engines": {
|
| 231 |
+
"node": ">=6.9.0"
|
| 232 |
+
},
|
| 233 |
+
"peerDependencies": {
|
| 234 |
+
"@babel/core": "^7.0.0-0"
|
| 235 |
+
}
|
| 236 |
+
},
|
| 237 |
+
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
| 238 |
+
"version": "7.27.1",
|
| 239 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
| 240 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 241 |
+
"dev": true,
|
| 242 |
+
"license": "MIT",
|
| 243 |
+
"dependencies": {
|
| 244 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 245 |
+
},
|
| 246 |
+
"engines": {
|
| 247 |
+
"node": ">=6.9.0"
|
| 248 |
+
},
|
| 249 |
+
"peerDependencies": {
|
| 250 |
+
"@babel/core": "^7.0.0-0"
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"node_modules/@babel/template": {
|
| 254 |
+
"version": "7.28.6",
|
| 255 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 256 |
+
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 257 |
+
"dev": true,
|
| 258 |
+
"license": "MIT",
|
| 259 |
+
"dependencies": {
|
| 260 |
+
"@babel/code-frame": "^7.28.6",
|
| 261 |
+
"@babel/parser": "^7.28.6",
|
| 262 |
+
"@babel/types": "^7.28.6"
|
| 263 |
+
},
|
| 264 |
+
"engines": {
|
| 265 |
+
"node": ">=6.9.0"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"node_modules/@babel/traverse": {
|
| 269 |
+
"version": "7.29.0",
|
| 270 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 271 |
+
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 272 |
+
"dev": true,
|
| 273 |
+
"license": "MIT",
|
| 274 |
+
"dependencies": {
|
| 275 |
+
"@babel/code-frame": "^7.29.0",
|
| 276 |
+
"@babel/generator": "^7.29.0",
|
| 277 |
+
"@babel/helper-globals": "^7.28.0",
|
| 278 |
+
"@babel/parser": "^7.29.0",
|
| 279 |
+
"@babel/template": "^7.28.6",
|
| 280 |
+
"@babel/types": "^7.29.0",
|
| 281 |
+
"debug": "^4.3.1"
|
| 282 |
+
},
|
| 283 |
+
"engines": {
|
| 284 |
+
"node": ">=6.9.0"
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
"node_modules/@babel/types": {
|
| 288 |
+
"version": "7.29.0",
|
| 289 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 290 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 291 |
+
"dev": true,
|
| 292 |
+
"license": "MIT",
|
| 293 |
+
"dependencies": {
|
| 294 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 295 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 296 |
+
},
|
| 297 |
+
"engines": {
|
| 298 |
+
"node": ">=6.9.0"
|
| 299 |
+
}
|
| 300 |
+
},
|
| 301 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 302 |
+
"version": "0.21.5",
|
| 303 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 304 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 305 |
+
"cpu": [
|
| 306 |
+
"ppc64"
|
| 307 |
+
],
|
| 308 |
+
"dev": true,
|
| 309 |
+
"license": "MIT",
|
| 310 |
+
"optional": true,
|
| 311 |
+
"os": [
|
| 312 |
+
"aix"
|
| 313 |
+
],
|
| 314 |
+
"engines": {
|
| 315 |
+
"node": ">=12"
|
| 316 |
+
}
|
| 317 |
+
},
|
| 318 |
+
"node_modules/@esbuild/android-arm": {
|
| 319 |
+
"version": "0.21.5",
|
| 320 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 321 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 322 |
+
"cpu": [
|
| 323 |
+
"arm"
|
| 324 |
+
],
|
| 325 |
+
"dev": true,
|
| 326 |
+
"license": "MIT",
|
| 327 |
+
"optional": true,
|
| 328 |
+
"os": [
|
| 329 |
+
"android"
|
| 330 |
+
],
|
| 331 |
+
"engines": {
|
| 332 |
+
"node": ">=12"
|
| 333 |
+
}
|
| 334 |
+
},
|
| 335 |
+
"node_modules/@esbuild/android-arm64": {
|
| 336 |
+
"version": "0.21.5",
|
| 337 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 338 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 339 |
+
"cpu": [
|
| 340 |
+
"arm64"
|
| 341 |
+
],
|
| 342 |
+
"dev": true,
|
| 343 |
+
"license": "MIT",
|
| 344 |
+
"optional": true,
|
| 345 |
+
"os": [
|
| 346 |
+
"android"
|
| 347 |
+
],
|
| 348 |
+
"engines": {
|
| 349 |
+
"node": ">=12"
|
| 350 |
+
}
|
| 351 |
+
},
|
| 352 |
+
"node_modules/@esbuild/android-x64": {
|
| 353 |
+
"version": "0.21.5",
|
| 354 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 355 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 356 |
+
"cpu": [
|
| 357 |
+
"x64"
|
| 358 |
+
],
|
| 359 |
+
"dev": true,
|
| 360 |
+
"license": "MIT",
|
| 361 |
+
"optional": true,
|
| 362 |
+
"os": [
|
| 363 |
+
"android"
|
| 364 |
+
],
|
| 365 |
+
"engines": {
|
| 366 |
+
"node": ">=12"
|
| 367 |
+
}
|
| 368 |
+
},
|
| 369 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 370 |
+
"version": "0.21.5",
|
| 371 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 372 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 373 |
+
"cpu": [
|
| 374 |
+
"arm64"
|
| 375 |
+
],
|
| 376 |
+
"dev": true,
|
| 377 |
+
"license": "MIT",
|
| 378 |
+
"optional": true,
|
| 379 |
+
"os": [
|
| 380 |
+
"darwin"
|
| 381 |
+
],
|
| 382 |
+
"engines": {
|
| 383 |
+
"node": ">=12"
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 387 |
+
"version": "0.21.5",
|
| 388 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 389 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 390 |
+
"cpu": [
|
| 391 |
+
"x64"
|
| 392 |
+
],
|
| 393 |
+
"dev": true,
|
| 394 |
+
"license": "MIT",
|
| 395 |
+
"optional": true,
|
| 396 |
+
"os": [
|
| 397 |
+
"darwin"
|
| 398 |
+
],
|
| 399 |
+
"engines": {
|
| 400 |
+
"node": ">=12"
|
| 401 |
+
}
|
| 402 |
+
},
|
| 403 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 404 |
+
"version": "0.21.5",
|
| 405 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 406 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 407 |
+
"cpu": [
|
| 408 |
+
"arm64"
|
| 409 |
+
],
|
| 410 |
+
"dev": true,
|
| 411 |
+
"license": "MIT",
|
| 412 |
+
"optional": true,
|
| 413 |
+
"os": [
|
| 414 |
+
"freebsd"
|
| 415 |
+
],
|
| 416 |
+
"engines": {
|
| 417 |
+
"node": ">=12"
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 421 |
+
"version": "0.21.5",
|
| 422 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 423 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 424 |
+
"cpu": [
|
| 425 |
+
"x64"
|
| 426 |
+
],
|
| 427 |
+
"dev": true,
|
| 428 |
+
"license": "MIT",
|
| 429 |
+
"optional": true,
|
| 430 |
+
"os": [
|
| 431 |
+
"freebsd"
|
| 432 |
+
],
|
| 433 |
+
"engines": {
|
| 434 |
+
"node": ">=12"
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"node_modules/@esbuild/linux-arm": {
|
| 438 |
+
"version": "0.21.5",
|
| 439 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 440 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 441 |
+
"cpu": [
|
| 442 |
+
"arm"
|
| 443 |
+
],
|
| 444 |
+
"dev": true,
|
| 445 |
+
"license": "MIT",
|
| 446 |
+
"optional": true,
|
| 447 |
+
"os": [
|
| 448 |
+
"linux"
|
| 449 |
+
],
|
| 450 |
+
"engines": {
|
| 451 |
+
"node": ">=12"
|
| 452 |
+
}
|
| 453 |
+
},
|
| 454 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 455 |
+
"version": "0.21.5",
|
| 456 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 457 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 458 |
+
"cpu": [
|
| 459 |
+
"arm64"
|
| 460 |
+
],
|
| 461 |
+
"dev": true,
|
| 462 |
+
"license": "MIT",
|
| 463 |
+
"optional": true,
|
| 464 |
+
"os": [
|
| 465 |
+
"linux"
|
| 466 |
+
],
|
| 467 |
+
"engines": {
|
| 468 |
+
"node": ">=12"
|
| 469 |
+
}
|
| 470 |
+
},
|
| 471 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 472 |
+
"version": "0.21.5",
|
| 473 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 474 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 475 |
+
"cpu": [
|
| 476 |
+
"ia32"
|
| 477 |
+
],
|
| 478 |
+
"dev": true,
|
| 479 |
+
"license": "MIT",
|
| 480 |
+
"optional": true,
|
| 481 |
+
"os": [
|
| 482 |
+
"linux"
|
| 483 |
+
],
|
| 484 |
+
"engines": {
|
| 485 |
+
"node": ">=12"
|
| 486 |
+
}
|
| 487 |
+
},
|
| 488 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 489 |
+
"version": "0.21.5",
|
| 490 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 491 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 492 |
+
"cpu": [
|
| 493 |
+
"loong64"
|
| 494 |
+
],
|
| 495 |
+
"dev": true,
|
| 496 |
+
"license": "MIT",
|
| 497 |
+
"optional": true,
|
| 498 |
+
"os": [
|
| 499 |
+
"linux"
|
| 500 |
+
],
|
| 501 |
+
"engines": {
|
| 502 |
+
"node": ">=12"
|
| 503 |
+
}
|
| 504 |
+
},
|
| 505 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 506 |
+
"version": "0.21.5",
|
| 507 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 508 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 509 |
+
"cpu": [
|
| 510 |
+
"mips64el"
|
| 511 |
+
],
|
| 512 |
+
"dev": true,
|
| 513 |
+
"license": "MIT",
|
| 514 |
+
"optional": true,
|
| 515 |
+
"os": [
|
| 516 |
+
"linux"
|
| 517 |
+
],
|
| 518 |
+
"engines": {
|
| 519 |
+
"node": ">=12"
|
| 520 |
+
}
|
| 521 |
+
},
|
| 522 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 523 |
+
"version": "0.21.5",
|
| 524 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 525 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 526 |
+
"cpu": [
|
| 527 |
+
"ppc64"
|
| 528 |
+
],
|
| 529 |
+
"dev": true,
|
| 530 |
+
"license": "MIT",
|
| 531 |
+
"optional": true,
|
| 532 |
+
"os": [
|
| 533 |
+
"linux"
|
| 534 |
+
],
|
| 535 |
+
"engines": {
|
| 536 |
+
"node": ">=12"
|
| 537 |
+
}
|
| 538 |
+
},
|
| 539 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 540 |
+
"version": "0.21.5",
|
| 541 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 542 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 543 |
+
"cpu": [
|
| 544 |
+
"riscv64"
|
| 545 |
+
],
|
| 546 |
+
"dev": true,
|
| 547 |
+
"license": "MIT",
|
| 548 |
+
"optional": true,
|
| 549 |
+
"os": [
|
| 550 |
+
"linux"
|
| 551 |
+
],
|
| 552 |
+
"engines": {
|
| 553 |
+
"node": ">=12"
|
| 554 |
+
}
|
| 555 |
+
},
|
| 556 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 557 |
+
"version": "0.21.5",
|
| 558 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 559 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 560 |
+
"cpu": [
|
| 561 |
+
"s390x"
|
| 562 |
+
],
|
| 563 |
+
"dev": true,
|
| 564 |
+
"license": "MIT",
|
| 565 |
+
"optional": true,
|
| 566 |
+
"os": [
|
| 567 |
+
"linux"
|
| 568 |
+
],
|
| 569 |
+
"engines": {
|
| 570 |
+
"node": ">=12"
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"node_modules/@esbuild/linux-x64": {
|
| 574 |
+
"version": "0.21.5",
|
| 575 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 576 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 577 |
+
"cpu": [
|
| 578 |
+
"x64"
|
| 579 |
+
],
|
| 580 |
+
"dev": true,
|
| 581 |
+
"license": "MIT",
|
| 582 |
+
"optional": true,
|
| 583 |
+
"os": [
|
| 584 |
+
"linux"
|
| 585 |
+
],
|
| 586 |
+
"engines": {
|
| 587 |
+
"node": ">=12"
|
| 588 |
+
}
|
| 589 |
+
},
|
| 590 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 591 |
+
"version": "0.21.5",
|
| 592 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 593 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 594 |
+
"cpu": [
|
| 595 |
+
"x64"
|
| 596 |
+
],
|
| 597 |
+
"dev": true,
|
| 598 |
+
"license": "MIT",
|
| 599 |
+
"optional": true,
|
| 600 |
+
"os": [
|
| 601 |
+
"netbsd"
|
| 602 |
+
],
|
| 603 |
+
"engines": {
|
| 604 |
+
"node": ">=12"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 608 |
+
"version": "0.21.5",
|
| 609 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 610 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 611 |
+
"cpu": [
|
| 612 |
+
"x64"
|
| 613 |
+
],
|
| 614 |
+
"dev": true,
|
| 615 |
+
"license": "MIT",
|
| 616 |
+
"optional": true,
|
| 617 |
+
"os": [
|
| 618 |
+
"openbsd"
|
| 619 |
+
],
|
| 620 |
+
"engines": {
|
| 621 |
+
"node": ">=12"
|
| 622 |
+
}
|
| 623 |
+
},
|
| 624 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 625 |
+
"version": "0.21.5",
|
| 626 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 627 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 628 |
+
"cpu": [
|
| 629 |
+
"x64"
|
| 630 |
+
],
|
| 631 |
+
"dev": true,
|
| 632 |
+
"license": "MIT",
|
| 633 |
+
"optional": true,
|
| 634 |
+
"os": [
|
| 635 |
+
"sunos"
|
| 636 |
+
],
|
| 637 |
+
"engines": {
|
| 638 |
+
"node": ">=12"
|
| 639 |
+
}
|
| 640 |
+
},
|
| 641 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 642 |
+
"version": "0.21.5",
|
| 643 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 644 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 645 |
+
"cpu": [
|
| 646 |
+
"arm64"
|
| 647 |
+
],
|
| 648 |
+
"dev": true,
|
| 649 |
+
"license": "MIT",
|
| 650 |
+
"optional": true,
|
| 651 |
+
"os": [
|
| 652 |
+
"win32"
|
| 653 |
+
],
|
| 654 |
+
"engines": {
|
| 655 |
+
"node": ">=12"
|
| 656 |
+
}
|
| 657 |
+
},
|
| 658 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 659 |
+
"version": "0.21.5",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 661 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 662 |
+
"cpu": [
|
| 663 |
+
"ia32"
|
| 664 |
+
],
|
| 665 |
+
"dev": true,
|
| 666 |
+
"license": "MIT",
|
| 667 |
+
"optional": true,
|
| 668 |
+
"os": [
|
| 669 |
+
"win32"
|
| 670 |
+
],
|
| 671 |
+
"engines": {
|
| 672 |
+
"node": ">=12"
|
| 673 |
+
}
|
| 674 |
+
},
|
| 675 |
+
"node_modules/@esbuild/win32-x64": {
|
| 676 |
+
"version": "0.21.5",
|
| 677 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 678 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 679 |
+
"cpu": [
|
| 680 |
+
"x64"
|
| 681 |
+
],
|
| 682 |
+
"dev": true,
|
| 683 |
+
"license": "MIT",
|
| 684 |
+
"optional": true,
|
| 685 |
+
"os": [
|
| 686 |
+
"win32"
|
| 687 |
+
],
|
| 688 |
+
"engines": {
|
| 689 |
+
"node": ">=12"
|
| 690 |
+
}
|
| 691 |
+
},
|
| 692 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 693 |
+
"version": "0.3.13",
|
| 694 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 695 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 696 |
+
"dev": true,
|
| 697 |
+
"license": "MIT",
|
| 698 |
+
"dependencies": {
|
| 699 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 700 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 701 |
+
}
|
| 702 |
+
},
|
| 703 |
+
"node_modules/@jridgewell/remapping": {
|
| 704 |
+
"version": "2.3.5",
|
| 705 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 706 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 707 |
+
"dev": true,
|
| 708 |
+
"license": "MIT",
|
| 709 |
+
"dependencies": {
|
| 710 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 711 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 712 |
+
}
|
| 713 |
+
},
|
| 714 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 715 |
+
"version": "3.1.2",
|
| 716 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 717 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 718 |
+
"dev": true,
|
| 719 |
+
"license": "MIT",
|
| 720 |
+
"engines": {
|
| 721 |
+
"node": ">=6.0.0"
|
| 722 |
+
}
|
| 723 |
+
},
|
| 724 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 725 |
+
"version": "1.5.5",
|
| 726 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 727 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 728 |
+
"dev": true,
|
| 729 |
+
"license": "MIT"
|
| 730 |
+
},
|
| 731 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 732 |
+
"version": "0.3.31",
|
| 733 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 734 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 735 |
+
"dev": true,
|
| 736 |
+
"license": "MIT",
|
| 737 |
+
"dependencies": {
|
| 738 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 739 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 740 |
+
}
|
| 741 |
+
},
|
| 742 |
+
"node_modules/@rolldown/pluginutils": {
|
| 743 |
+
"version": "1.0.0-beta.27",
|
| 744 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
| 745 |
+
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
| 746 |
+
"dev": true,
|
| 747 |
+
"license": "MIT"
|
| 748 |
+
},
|
| 749 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 750 |
+
"version": "4.60.3",
|
| 751 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
|
| 752 |
+
"integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
|
| 753 |
+
"cpu": [
|
| 754 |
+
"arm"
|
| 755 |
+
],
|
| 756 |
+
"dev": true,
|
| 757 |
+
"license": "MIT",
|
| 758 |
+
"optional": true,
|
| 759 |
+
"os": [
|
| 760 |
+
"android"
|
| 761 |
+
]
|
| 762 |
+
},
|
| 763 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 764 |
+
"version": "4.60.3",
|
| 765 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
|
| 766 |
+
"integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
|
| 767 |
+
"cpu": [
|
| 768 |
+
"arm64"
|
| 769 |
+
],
|
| 770 |
+
"dev": true,
|
| 771 |
+
"license": "MIT",
|
| 772 |
+
"optional": true,
|
| 773 |
+
"os": [
|
| 774 |
+
"android"
|
| 775 |
+
]
|
| 776 |
+
},
|
| 777 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 778 |
+
"version": "4.60.3",
|
| 779 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
|
| 780 |
+
"integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
|
| 781 |
+
"cpu": [
|
| 782 |
+
"arm64"
|
| 783 |
+
],
|
| 784 |
+
"dev": true,
|
| 785 |
+
"license": "MIT",
|
| 786 |
+
"optional": true,
|
| 787 |
+
"os": [
|
| 788 |
+
"darwin"
|
| 789 |
+
]
|
| 790 |
+
},
|
| 791 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 792 |
+
"version": "4.60.3",
|
| 793 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
|
| 794 |
+
"integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
|
| 795 |
+
"cpu": [
|
| 796 |
+
"x64"
|
| 797 |
+
],
|
| 798 |
+
"dev": true,
|
| 799 |
+
"license": "MIT",
|
| 800 |
+
"optional": true,
|
| 801 |
+
"os": [
|
| 802 |
+
"darwin"
|
| 803 |
+
]
|
| 804 |
+
},
|
| 805 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 806 |
+
"version": "4.60.3",
|
| 807 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
|
| 808 |
+
"integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
|
| 809 |
+
"cpu": [
|
| 810 |
+
"arm64"
|
| 811 |
+
],
|
| 812 |
+
"dev": true,
|
| 813 |
+
"license": "MIT",
|
| 814 |
+
"optional": true,
|
| 815 |
+
"os": [
|
| 816 |
+
"freebsd"
|
| 817 |
+
]
|
| 818 |
+
},
|
| 819 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 820 |
+
"version": "4.60.3",
|
| 821 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
|
| 822 |
+
"integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
|
| 823 |
+
"cpu": [
|
| 824 |
+
"x64"
|
| 825 |
+
],
|
| 826 |
+
"dev": true,
|
| 827 |
+
"license": "MIT",
|
| 828 |
+
"optional": true,
|
| 829 |
+
"os": [
|
| 830 |
+
"freebsd"
|
| 831 |
+
]
|
| 832 |
+
},
|
| 833 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 834 |
+
"version": "4.60.3",
|
| 835 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
|
| 836 |
+
"integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
|
| 837 |
+
"cpu": [
|
| 838 |
+
"arm"
|
| 839 |
+
],
|
| 840 |
+
"dev": true,
|
| 841 |
+
"license": "MIT",
|
| 842 |
+
"optional": true,
|
| 843 |
+
"os": [
|
| 844 |
+
"linux"
|
| 845 |
+
]
|
| 846 |
+
},
|
| 847 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 848 |
+
"version": "4.60.3",
|
| 849 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
|
| 850 |
+
"integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
|
| 851 |
+
"cpu": [
|
| 852 |
+
"arm"
|
| 853 |
+
],
|
| 854 |
+
"dev": true,
|
| 855 |
+
"license": "MIT",
|
| 856 |
+
"optional": true,
|
| 857 |
+
"os": [
|
| 858 |
+
"linux"
|
| 859 |
+
]
|
| 860 |
+
},
|
| 861 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 862 |
+
"version": "4.60.3",
|
| 863 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
|
| 864 |
+
"integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
|
| 865 |
+
"cpu": [
|
| 866 |
+
"arm64"
|
| 867 |
+
],
|
| 868 |
+
"dev": true,
|
| 869 |
+
"license": "MIT",
|
| 870 |
+
"optional": true,
|
| 871 |
+
"os": [
|
| 872 |
+
"linux"
|
| 873 |
+
]
|
| 874 |
+
},
|
| 875 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 876 |
+
"version": "4.60.3",
|
| 877 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
|
| 878 |
+
"integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
|
| 879 |
+
"cpu": [
|
| 880 |
+
"arm64"
|
| 881 |
+
],
|
| 882 |
+
"dev": true,
|
| 883 |
+
"license": "MIT",
|
| 884 |
+
"optional": true,
|
| 885 |
+
"os": [
|
| 886 |
+
"linux"
|
| 887 |
+
]
|
| 888 |
+
},
|
| 889 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 890 |
+
"version": "4.60.3",
|
| 891 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
|
| 892 |
+
"integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
|
| 893 |
+
"cpu": [
|
| 894 |
+
"loong64"
|
| 895 |
+
],
|
| 896 |
+
"dev": true,
|
| 897 |
+
"license": "MIT",
|
| 898 |
+
"optional": true,
|
| 899 |
+
"os": [
|
| 900 |
+
"linux"
|
| 901 |
+
]
|
| 902 |
+
},
|
| 903 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 904 |
+
"version": "4.60.3",
|
| 905 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
|
| 906 |
+
"integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
|
| 907 |
+
"cpu": [
|
| 908 |
+
"loong64"
|
| 909 |
+
],
|
| 910 |
+
"dev": true,
|
| 911 |
+
"license": "MIT",
|
| 912 |
+
"optional": true,
|
| 913 |
+
"os": [
|
| 914 |
+
"linux"
|
| 915 |
+
]
|
| 916 |
+
},
|
| 917 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 918 |
+
"version": "4.60.3",
|
| 919 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
|
| 920 |
+
"integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
|
| 921 |
+
"cpu": [
|
| 922 |
+
"ppc64"
|
| 923 |
+
],
|
| 924 |
+
"dev": true,
|
| 925 |
+
"license": "MIT",
|
| 926 |
+
"optional": true,
|
| 927 |
+
"os": [
|
| 928 |
+
"linux"
|
| 929 |
+
]
|
| 930 |
+
},
|
| 931 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 932 |
+
"version": "4.60.3",
|
| 933 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
|
| 934 |
+
"integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
|
| 935 |
+
"cpu": [
|
| 936 |
+
"ppc64"
|
| 937 |
+
],
|
| 938 |
+
"dev": true,
|
| 939 |
+
"license": "MIT",
|
| 940 |
+
"optional": true,
|
| 941 |
+
"os": [
|
| 942 |
+
"linux"
|
| 943 |
+
]
|
| 944 |
+
},
|
| 945 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 946 |
+
"version": "4.60.3",
|
| 947 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
|
| 948 |
+
"integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
|
| 949 |
+
"cpu": [
|
| 950 |
+
"riscv64"
|
| 951 |
+
],
|
| 952 |
+
"dev": true,
|
| 953 |
+
"license": "MIT",
|
| 954 |
+
"optional": true,
|
| 955 |
+
"os": [
|
| 956 |
+
"linux"
|
| 957 |
+
]
|
| 958 |
+
},
|
| 959 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 960 |
+
"version": "4.60.3",
|
| 961 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
|
| 962 |
+
"integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
|
| 963 |
+
"cpu": [
|
| 964 |
+
"riscv64"
|
| 965 |
+
],
|
| 966 |
+
"dev": true,
|
| 967 |
+
"license": "MIT",
|
| 968 |
+
"optional": true,
|
| 969 |
+
"os": [
|
| 970 |
+
"linux"
|
| 971 |
+
]
|
| 972 |
+
},
|
| 973 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 974 |
+
"version": "4.60.3",
|
| 975 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
|
| 976 |
+
"integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
|
| 977 |
+
"cpu": [
|
| 978 |
+
"s390x"
|
| 979 |
+
],
|
| 980 |
+
"dev": true,
|
| 981 |
+
"license": "MIT",
|
| 982 |
+
"optional": true,
|
| 983 |
+
"os": [
|
| 984 |
+
"linux"
|
| 985 |
+
]
|
| 986 |
+
},
|
| 987 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 988 |
+
"version": "4.60.3",
|
| 989 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
|
| 990 |
+
"integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
|
| 991 |
+
"cpu": [
|
| 992 |
+
"x64"
|
| 993 |
+
],
|
| 994 |
+
"dev": true,
|
| 995 |
+
"license": "MIT",
|
| 996 |
+
"optional": true,
|
| 997 |
+
"os": [
|
| 998 |
+
"linux"
|
| 999 |
+
]
|
| 1000 |
+
},
|
| 1001 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 1002 |
+
"version": "4.60.3",
|
| 1003 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
|
| 1004 |
+
"integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
|
| 1005 |
+
"cpu": [
|
| 1006 |
+
"x64"
|
| 1007 |
+
],
|
| 1008 |
+
"dev": true,
|
| 1009 |
+
"license": "MIT",
|
| 1010 |
+
"optional": true,
|
| 1011 |
+
"os": [
|
| 1012 |
+
"linux"
|
| 1013 |
+
]
|
| 1014 |
+
},
|
| 1015 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 1016 |
+
"version": "4.60.3",
|
| 1017 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
|
| 1018 |
+
"integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
|
| 1019 |
+
"cpu": [
|
| 1020 |
+
"x64"
|
| 1021 |
+
],
|
| 1022 |
+
"dev": true,
|
| 1023 |
+
"license": "MIT",
|
| 1024 |
+
"optional": true,
|
| 1025 |
+
"os": [
|
| 1026 |
+
"openbsd"
|
| 1027 |
+
]
|
| 1028 |
+
},
|
| 1029 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 1030 |
+
"version": "4.60.3",
|
| 1031 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
|
| 1032 |
+
"integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
|
| 1033 |
+
"cpu": [
|
| 1034 |
+
"arm64"
|
| 1035 |
+
],
|
| 1036 |
+
"dev": true,
|
| 1037 |
+
"license": "MIT",
|
| 1038 |
+
"optional": true,
|
| 1039 |
+
"os": [
|
| 1040 |
+
"openharmony"
|
| 1041 |
+
]
|
| 1042 |
+
},
|
| 1043 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1044 |
+
"version": "4.60.3",
|
| 1045 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
|
| 1046 |
+
"integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
|
| 1047 |
+
"cpu": [
|
| 1048 |
+
"arm64"
|
| 1049 |
+
],
|
| 1050 |
+
"dev": true,
|
| 1051 |
+
"license": "MIT",
|
| 1052 |
+
"optional": true,
|
| 1053 |
+
"os": [
|
| 1054 |
+
"win32"
|
| 1055 |
+
]
|
| 1056 |
+
},
|
| 1057 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1058 |
+
"version": "4.60.3",
|
| 1059 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
|
| 1060 |
+
"integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
|
| 1061 |
+
"cpu": [
|
| 1062 |
+
"ia32"
|
| 1063 |
+
],
|
| 1064 |
+
"dev": true,
|
| 1065 |
+
"license": "MIT",
|
| 1066 |
+
"optional": true,
|
| 1067 |
+
"os": [
|
| 1068 |
+
"win32"
|
| 1069 |
+
]
|
| 1070 |
+
},
|
| 1071 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1072 |
+
"version": "4.60.3",
|
| 1073 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
|
| 1074 |
+
"integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
|
| 1075 |
+
"cpu": [
|
| 1076 |
+
"x64"
|
| 1077 |
+
],
|
| 1078 |
+
"dev": true,
|
| 1079 |
+
"license": "MIT",
|
| 1080 |
+
"optional": true,
|
| 1081 |
+
"os": [
|
| 1082 |
+
"win32"
|
| 1083 |
+
]
|
| 1084 |
+
},
|
| 1085 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1086 |
+
"version": "4.60.3",
|
| 1087 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
|
| 1088 |
+
"integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
|
| 1089 |
+
"cpu": [
|
| 1090 |
+
"x64"
|
| 1091 |
+
],
|
| 1092 |
+
"dev": true,
|
| 1093 |
+
"license": "MIT",
|
| 1094 |
+
"optional": true,
|
| 1095 |
+
"os": [
|
| 1096 |
+
"win32"
|
| 1097 |
+
]
|
| 1098 |
+
},
|
| 1099 |
+
"node_modules/@types/babel__core": {
|
| 1100 |
+
"version": "7.20.5",
|
| 1101 |
+
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 1102 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 1103 |
+
"dev": true,
|
| 1104 |
+
"license": "MIT",
|
| 1105 |
+
"dependencies": {
|
| 1106 |
+
"@babel/parser": "^7.20.7",
|
| 1107 |
+
"@babel/types": "^7.20.7",
|
| 1108 |
+
"@types/babel__generator": "*",
|
| 1109 |
+
"@types/babel__template": "*",
|
| 1110 |
+
"@types/babel__traverse": "*"
|
| 1111 |
+
}
|
| 1112 |
+
},
|
| 1113 |
+
"node_modules/@types/babel__generator": {
|
| 1114 |
+
"version": "7.27.0",
|
| 1115 |
+
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 1116 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 1117 |
+
"dev": true,
|
| 1118 |
+
"license": "MIT",
|
| 1119 |
+
"dependencies": {
|
| 1120 |
+
"@babel/types": "^7.0.0"
|
| 1121 |
+
}
|
| 1122 |
+
},
|
| 1123 |
+
"node_modules/@types/babel__template": {
|
| 1124 |
+
"version": "7.4.4",
|
| 1125 |
+
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 1126 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 1127 |
+
"dev": true,
|
| 1128 |
+
"license": "MIT",
|
| 1129 |
+
"dependencies": {
|
| 1130 |
+
"@babel/parser": "^7.1.0",
|
| 1131 |
+
"@babel/types": "^7.0.0"
|
| 1132 |
+
}
|
| 1133 |
+
},
|
| 1134 |
+
"node_modules/@types/babel__traverse": {
|
| 1135 |
+
"version": "7.28.0",
|
| 1136 |
+
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 1137 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 1138 |
+
"dev": true,
|
| 1139 |
+
"license": "MIT",
|
| 1140 |
+
"dependencies": {
|
| 1141 |
+
"@babel/types": "^7.28.2"
|
| 1142 |
+
}
|
| 1143 |
+
},
|
| 1144 |
+
"node_modules/@types/estree": {
|
| 1145 |
+
"version": "1.0.8",
|
| 1146 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1147 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1148 |
+
"dev": true,
|
| 1149 |
+
"license": "MIT"
|
| 1150 |
+
},
|
| 1151 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1152 |
+
"version": "4.7.0",
|
| 1153 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
| 1154 |
+
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
| 1155 |
+
"dev": true,
|
| 1156 |
+
"license": "MIT",
|
| 1157 |
+
"dependencies": {
|
| 1158 |
+
"@babel/core": "^7.28.0",
|
| 1159 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 1160 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 1161 |
+
"@rolldown/pluginutils": "1.0.0-beta.27",
|
| 1162 |
+
"@types/babel__core": "^7.20.5",
|
| 1163 |
+
"react-refresh": "^0.17.0"
|
| 1164 |
+
},
|
| 1165 |
+
"engines": {
|
| 1166 |
+
"node": "^14.18.0 || >=16.0.0"
|
| 1167 |
+
},
|
| 1168 |
+
"peerDependencies": {
|
| 1169 |
+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1170 |
+
}
|
| 1171 |
+
},
|
| 1172 |
+
"node_modules/baseline-browser-mapping": {
|
| 1173 |
+
"version": "2.10.29",
|
| 1174 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
|
| 1175 |
+
"integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
|
| 1176 |
+
"dev": true,
|
| 1177 |
+
"license": "Apache-2.0",
|
| 1178 |
+
"bin": {
|
| 1179 |
+
"baseline-browser-mapping": "dist/cli.cjs"
|
| 1180 |
+
},
|
| 1181 |
+
"engines": {
|
| 1182 |
+
"node": ">=6.0.0"
|
| 1183 |
+
}
|
| 1184 |
+
},
|
| 1185 |
+
"node_modules/browserslist": {
|
| 1186 |
+
"version": "4.28.2",
|
| 1187 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
| 1188 |
+
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
| 1189 |
+
"dev": true,
|
| 1190 |
+
"funding": [
|
| 1191 |
+
{
|
| 1192 |
+
"type": "opencollective",
|
| 1193 |
+
"url": "https://opencollective.com/browserslist"
|
| 1194 |
+
},
|
| 1195 |
+
{
|
| 1196 |
+
"type": "tidelift",
|
| 1197 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1198 |
+
},
|
| 1199 |
+
{
|
| 1200 |
+
"type": "github",
|
| 1201 |
+
"url": "https://github.com/sponsors/ai"
|
| 1202 |
+
}
|
| 1203 |
+
],
|
| 1204 |
+
"license": "MIT",
|
| 1205 |
+
"dependencies": {
|
| 1206 |
+
"baseline-browser-mapping": "^2.10.12",
|
| 1207 |
+
"caniuse-lite": "^1.0.30001782",
|
| 1208 |
+
"electron-to-chromium": "^1.5.328",
|
| 1209 |
+
"node-releases": "^2.0.36",
|
| 1210 |
+
"update-browserslist-db": "^1.2.3"
|
| 1211 |
+
},
|
| 1212 |
+
"bin": {
|
| 1213 |
+
"browserslist": "cli.js"
|
| 1214 |
+
},
|
| 1215 |
+
"engines": {
|
| 1216 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1217 |
+
}
|
| 1218 |
+
},
|
| 1219 |
+
"node_modules/caniuse-lite": {
|
| 1220 |
+
"version": "1.0.30001792",
|
| 1221 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
|
| 1222 |
+
"integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
|
| 1223 |
+
"dev": true,
|
| 1224 |
+
"funding": [
|
| 1225 |
+
{
|
| 1226 |
+
"type": "opencollective",
|
| 1227 |
+
"url": "https://opencollective.com/browserslist"
|
| 1228 |
+
},
|
| 1229 |
+
{
|
| 1230 |
+
"type": "tidelift",
|
| 1231 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1232 |
+
},
|
| 1233 |
+
{
|
| 1234 |
+
"type": "github",
|
| 1235 |
+
"url": "https://github.com/sponsors/ai"
|
| 1236 |
+
}
|
| 1237 |
+
],
|
| 1238 |
+
"license": "CC-BY-4.0"
|
| 1239 |
+
},
|
| 1240 |
+
"node_modules/convert-source-map": {
|
| 1241 |
+
"version": "2.0.0",
|
| 1242 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1243 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1244 |
+
"dev": true,
|
| 1245 |
+
"license": "MIT"
|
| 1246 |
+
},
|
| 1247 |
+
"node_modules/debug": {
|
| 1248 |
+
"version": "4.4.3",
|
| 1249 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1250 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1251 |
+
"dev": true,
|
| 1252 |
+
"license": "MIT",
|
| 1253 |
+
"dependencies": {
|
| 1254 |
+
"ms": "^2.1.3"
|
| 1255 |
+
},
|
| 1256 |
+
"engines": {
|
| 1257 |
+
"node": ">=6.0"
|
| 1258 |
+
},
|
| 1259 |
+
"peerDependenciesMeta": {
|
| 1260 |
+
"supports-color": {
|
| 1261 |
+
"optional": true
|
| 1262 |
+
}
|
| 1263 |
+
}
|
| 1264 |
+
},
|
| 1265 |
+
"node_modules/electron-to-chromium": {
|
| 1266 |
+
"version": "1.5.353",
|
| 1267 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
|
| 1268 |
+
"integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
|
| 1269 |
+
"dev": true,
|
| 1270 |
+
"license": "ISC"
|
| 1271 |
+
},
|
| 1272 |
+
"node_modules/esbuild": {
|
| 1273 |
+
"version": "0.21.5",
|
| 1274 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 1275 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 1276 |
+
"dev": true,
|
| 1277 |
+
"hasInstallScript": true,
|
| 1278 |
+
"license": "MIT",
|
| 1279 |
+
"bin": {
|
| 1280 |
+
"esbuild": "bin/esbuild"
|
| 1281 |
+
},
|
| 1282 |
+
"engines": {
|
| 1283 |
+
"node": ">=12"
|
| 1284 |
+
},
|
| 1285 |
+
"optionalDependencies": {
|
| 1286 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 1287 |
+
"@esbuild/android-arm": "0.21.5",
|
| 1288 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 1289 |
+
"@esbuild/android-x64": "0.21.5",
|
| 1290 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 1291 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 1292 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 1293 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 1294 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 1295 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 1296 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 1297 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 1298 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 1299 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 1300 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 1301 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 1302 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 1303 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 1304 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 1305 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 1306 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 1307 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 1308 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 1309 |
+
}
|
| 1310 |
+
},
|
| 1311 |
+
"node_modules/escalade": {
|
| 1312 |
+
"version": "3.2.0",
|
| 1313 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1314 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1315 |
+
"dev": true,
|
| 1316 |
+
"license": "MIT",
|
| 1317 |
+
"engines": {
|
| 1318 |
+
"node": ">=6"
|
| 1319 |
+
}
|
| 1320 |
+
},
|
| 1321 |
+
"node_modules/fsevents": {
|
| 1322 |
+
"version": "2.3.3",
|
| 1323 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1324 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1325 |
+
"dev": true,
|
| 1326 |
+
"hasInstallScript": true,
|
| 1327 |
+
"license": "MIT",
|
| 1328 |
+
"optional": true,
|
| 1329 |
+
"os": [
|
| 1330 |
+
"darwin"
|
| 1331 |
+
],
|
| 1332 |
+
"engines": {
|
| 1333 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1334 |
+
}
|
| 1335 |
+
},
|
| 1336 |
+
"node_modules/gensync": {
|
| 1337 |
+
"version": "1.0.0-beta.2",
|
| 1338 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1339 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1340 |
+
"dev": true,
|
| 1341 |
+
"license": "MIT",
|
| 1342 |
+
"engines": {
|
| 1343 |
+
"node": ">=6.9.0"
|
| 1344 |
+
}
|
| 1345 |
+
},
|
| 1346 |
+
"node_modules/js-tokens": {
|
| 1347 |
+
"version": "4.0.0",
|
| 1348 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1349 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1350 |
+
"license": "MIT"
|
| 1351 |
+
},
|
| 1352 |
+
"node_modules/jsesc": {
|
| 1353 |
+
"version": "3.1.0",
|
| 1354 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 1355 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1356 |
+
"dev": true,
|
| 1357 |
+
"license": "MIT",
|
| 1358 |
+
"bin": {
|
| 1359 |
+
"jsesc": "bin/jsesc"
|
| 1360 |
+
},
|
| 1361 |
+
"engines": {
|
| 1362 |
+
"node": ">=6"
|
| 1363 |
+
}
|
| 1364 |
+
},
|
| 1365 |
+
"node_modules/json5": {
|
| 1366 |
+
"version": "2.2.3",
|
| 1367 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 1368 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1369 |
+
"dev": true,
|
| 1370 |
+
"license": "MIT",
|
| 1371 |
+
"bin": {
|
| 1372 |
+
"json5": "lib/cli.js"
|
| 1373 |
+
},
|
| 1374 |
+
"engines": {
|
| 1375 |
+
"node": ">=6"
|
| 1376 |
+
}
|
| 1377 |
+
},
|
| 1378 |
+
"node_modules/loose-envify": {
|
| 1379 |
+
"version": "1.4.0",
|
| 1380 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 1381 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 1382 |
+
"license": "MIT",
|
| 1383 |
+
"dependencies": {
|
| 1384 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 1385 |
+
},
|
| 1386 |
+
"bin": {
|
| 1387 |
+
"loose-envify": "cli.js"
|
| 1388 |
+
}
|
| 1389 |
+
},
|
| 1390 |
+
"node_modules/lru-cache": {
|
| 1391 |
+
"version": "5.1.1",
|
| 1392 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 1393 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 1394 |
+
"dev": true,
|
| 1395 |
+
"license": "ISC",
|
| 1396 |
+
"dependencies": {
|
| 1397 |
+
"yallist": "^3.0.2"
|
| 1398 |
+
}
|
| 1399 |
+
},
|
| 1400 |
+
"node_modules/ms": {
|
| 1401 |
+
"version": "2.1.3",
|
| 1402 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1403 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1404 |
+
"dev": true,
|
| 1405 |
+
"license": "MIT"
|
| 1406 |
+
},
|
| 1407 |
+
"node_modules/nanoid": {
|
| 1408 |
+
"version": "3.3.12",
|
| 1409 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
| 1410 |
+
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
| 1411 |
+
"dev": true,
|
| 1412 |
+
"funding": [
|
| 1413 |
+
{
|
| 1414 |
+
"type": "github",
|
| 1415 |
+
"url": "https://github.com/sponsors/ai"
|
| 1416 |
+
}
|
| 1417 |
+
],
|
| 1418 |
+
"license": "MIT",
|
| 1419 |
+
"bin": {
|
| 1420 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1421 |
+
},
|
| 1422 |
+
"engines": {
|
| 1423 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1424 |
+
}
|
| 1425 |
+
},
|
| 1426 |
+
"node_modules/node-releases": {
|
| 1427 |
+
"version": "2.0.44",
|
| 1428 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
|
| 1429 |
+
"integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
|
| 1430 |
+
"dev": true,
|
| 1431 |
+
"license": "MIT"
|
| 1432 |
+
},
|
| 1433 |
+
"node_modules/picocolors": {
|
| 1434 |
+
"version": "1.1.1",
|
| 1435 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1436 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1437 |
+
"dev": true,
|
| 1438 |
+
"license": "ISC"
|
| 1439 |
+
},
|
| 1440 |
+
"node_modules/postcss": {
|
| 1441 |
+
"version": "8.5.14",
|
| 1442 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
| 1443 |
+
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
| 1444 |
+
"dev": true,
|
| 1445 |
+
"funding": [
|
| 1446 |
+
{
|
| 1447 |
+
"type": "opencollective",
|
| 1448 |
+
"url": "https://opencollective.com/postcss/"
|
| 1449 |
+
},
|
| 1450 |
+
{
|
| 1451 |
+
"type": "tidelift",
|
| 1452 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1453 |
+
},
|
| 1454 |
+
{
|
| 1455 |
+
"type": "github",
|
| 1456 |
+
"url": "https://github.com/sponsors/ai"
|
| 1457 |
+
}
|
| 1458 |
+
],
|
| 1459 |
+
"license": "MIT",
|
| 1460 |
+
"dependencies": {
|
| 1461 |
+
"nanoid": "^3.3.11",
|
| 1462 |
+
"picocolors": "^1.1.1",
|
| 1463 |
+
"source-map-js": "^1.2.1"
|
| 1464 |
+
},
|
| 1465 |
+
"engines": {
|
| 1466 |
+
"node": "^10 || ^12 || >=14"
|
| 1467 |
+
}
|
| 1468 |
+
},
|
| 1469 |
+
"node_modules/react": {
|
| 1470 |
+
"version": "18.3.1",
|
| 1471 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 1472 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 1473 |
+
"license": "MIT",
|
| 1474 |
+
"dependencies": {
|
| 1475 |
+
"loose-envify": "^1.1.0"
|
| 1476 |
+
},
|
| 1477 |
+
"engines": {
|
| 1478 |
+
"node": ">=0.10.0"
|
| 1479 |
+
}
|
| 1480 |
+
},
|
| 1481 |
+
"node_modules/react-dom": {
|
| 1482 |
+
"version": "18.3.1",
|
| 1483 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 1484 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 1485 |
+
"license": "MIT",
|
| 1486 |
+
"dependencies": {
|
| 1487 |
+
"loose-envify": "^1.1.0",
|
| 1488 |
+
"scheduler": "^0.23.2"
|
| 1489 |
+
},
|
| 1490 |
+
"peerDependencies": {
|
| 1491 |
+
"react": "^18.3.1"
|
| 1492 |
+
}
|
| 1493 |
+
},
|
| 1494 |
+
"node_modules/react-refresh": {
|
| 1495 |
+
"version": "0.17.0",
|
| 1496 |
+
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
| 1497 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
| 1498 |
+
"dev": true,
|
| 1499 |
+
"license": "MIT",
|
| 1500 |
+
"engines": {
|
| 1501 |
+
"node": ">=0.10.0"
|
| 1502 |
+
}
|
| 1503 |
+
},
|
| 1504 |
+
"node_modules/rollup": {
|
| 1505 |
+
"version": "4.60.3",
|
| 1506 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
|
| 1507 |
+
"integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
|
| 1508 |
+
"dev": true,
|
| 1509 |
+
"license": "MIT",
|
| 1510 |
+
"dependencies": {
|
| 1511 |
+
"@types/estree": "1.0.8"
|
| 1512 |
+
},
|
| 1513 |
+
"bin": {
|
| 1514 |
+
"rollup": "dist/bin/rollup"
|
| 1515 |
+
},
|
| 1516 |
+
"engines": {
|
| 1517 |
+
"node": ">=18.0.0",
|
| 1518 |
+
"npm": ">=8.0.0"
|
| 1519 |
+
},
|
| 1520 |
+
"optionalDependencies": {
|
| 1521 |
+
"@rollup/rollup-android-arm-eabi": "4.60.3",
|
| 1522 |
+
"@rollup/rollup-android-arm64": "4.60.3",
|
| 1523 |
+
"@rollup/rollup-darwin-arm64": "4.60.3",
|
| 1524 |
+
"@rollup/rollup-darwin-x64": "4.60.3",
|
| 1525 |
+
"@rollup/rollup-freebsd-arm64": "4.60.3",
|
| 1526 |
+
"@rollup/rollup-freebsd-x64": "4.60.3",
|
| 1527 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
|
| 1528 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.60.3",
|
| 1529 |
+
"@rollup/rollup-linux-arm64-gnu": "4.60.3",
|
| 1530 |
+
"@rollup/rollup-linux-arm64-musl": "4.60.3",
|
| 1531 |
+
"@rollup/rollup-linux-loong64-gnu": "4.60.3",
|
| 1532 |
+
"@rollup/rollup-linux-loong64-musl": "4.60.3",
|
| 1533 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.60.3",
|
| 1534 |
+
"@rollup/rollup-linux-ppc64-musl": "4.60.3",
|
| 1535 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.60.3",
|
| 1536 |
+
"@rollup/rollup-linux-riscv64-musl": "4.60.3",
|
| 1537 |
+
"@rollup/rollup-linux-s390x-gnu": "4.60.3",
|
| 1538 |
+
"@rollup/rollup-linux-x64-gnu": "4.60.3",
|
| 1539 |
+
"@rollup/rollup-linux-x64-musl": "4.60.3",
|
| 1540 |
+
"@rollup/rollup-openbsd-x64": "4.60.3",
|
| 1541 |
+
"@rollup/rollup-openharmony-arm64": "4.60.3",
|
| 1542 |
+
"@rollup/rollup-win32-arm64-msvc": "4.60.3",
|
| 1543 |
+
"@rollup/rollup-win32-ia32-msvc": "4.60.3",
|
| 1544 |
+
"@rollup/rollup-win32-x64-gnu": "4.60.3",
|
| 1545 |
+
"@rollup/rollup-win32-x64-msvc": "4.60.3",
|
| 1546 |
+
"fsevents": "~2.3.2"
|
| 1547 |
+
}
|
| 1548 |
+
},
|
| 1549 |
+
"node_modules/scheduler": {
|
| 1550 |
+
"version": "0.23.2",
|
| 1551 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 1552 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 1553 |
+
"license": "MIT",
|
| 1554 |
+
"dependencies": {
|
| 1555 |
+
"loose-envify": "^1.1.0"
|
| 1556 |
+
}
|
| 1557 |
+
},
|
| 1558 |
+
"node_modules/semver": {
|
| 1559 |
+
"version": "6.3.1",
|
| 1560 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 1561 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 1562 |
+
"dev": true,
|
| 1563 |
+
"license": "ISC",
|
| 1564 |
+
"bin": {
|
| 1565 |
+
"semver": "bin/semver.js"
|
| 1566 |
+
}
|
| 1567 |
+
},
|
| 1568 |
+
"node_modules/source-map-js": {
|
| 1569 |
+
"version": "1.2.1",
|
| 1570 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1571 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1572 |
+
"dev": true,
|
| 1573 |
+
"license": "BSD-3-Clause",
|
| 1574 |
+
"engines": {
|
| 1575 |
+
"node": ">=0.10.0"
|
| 1576 |
+
}
|
| 1577 |
+
},
|
| 1578 |
+
"node_modules/update-browserslist-db": {
|
| 1579 |
+
"version": "1.2.3",
|
| 1580 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 1581 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 1582 |
+
"dev": true,
|
| 1583 |
+
"funding": [
|
| 1584 |
+
{
|
| 1585 |
+
"type": "opencollective",
|
| 1586 |
+
"url": "https://opencollective.com/browserslist"
|
| 1587 |
+
},
|
| 1588 |
+
{
|
| 1589 |
+
"type": "tidelift",
|
| 1590 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1591 |
+
},
|
| 1592 |
+
{
|
| 1593 |
+
"type": "github",
|
| 1594 |
+
"url": "https://github.com/sponsors/ai"
|
| 1595 |
+
}
|
| 1596 |
+
],
|
| 1597 |
+
"license": "MIT",
|
| 1598 |
+
"dependencies": {
|
| 1599 |
+
"escalade": "^3.2.0",
|
| 1600 |
+
"picocolors": "^1.1.1"
|
| 1601 |
+
},
|
| 1602 |
+
"bin": {
|
| 1603 |
+
"update-browserslist-db": "cli.js"
|
| 1604 |
+
},
|
| 1605 |
+
"peerDependencies": {
|
| 1606 |
+
"browserslist": ">= 4.21.0"
|
| 1607 |
+
}
|
| 1608 |
+
},
|
| 1609 |
+
"node_modules/vite": {
|
| 1610 |
+
"version": "5.4.21",
|
| 1611 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
| 1612 |
+
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
| 1613 |
+
"dev": true,
|
| 1614 |
+
"license": "MIT",
|
| 1615 |
+
"dependencies": {
|
| 1616 |
+
"esbuild": "^0.21.3",
|
| 1617 |
+
"postcss": "^8.4.43",
|
| 1618 |
+
"rollup": "^4.20.0"
|
| 1619 |
+
},
|
| 1620 |
+
"bin": {
|
| 1621 |
+
"vite": "bin/vite.js"
|
| 1622 |
+
},
|
| 1623 |
+
"engines": {
|
| 1624 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 1625 |
+
},
|
| 1626 |
+
"funding": {
|
| 1627 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1628 |
+
},
|
| 1629 |
+
"optionalDependencies": {
|
| 1630 |
+
"fsevents": "~2.3.3"
|
| 1631 |
+
},
|
| 1632 |
+
"peerDependencies": {
|
| 1633 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 1634 |
+
"less": "*",
|
| 1635 |
+
"lightningcss": "^1.21.0",
|
| 1636 |
+
"sass": "*",
|
| 1637 |
+
"sass-embedded": "*",
|
| 1638 |
+
"stylus": "*",
|
| 1639 |
+
"sugarss": "*",
|
| 1640 |
+
"terser": "^5.4.0"
|
| 1641 |
+
},
|
| 1642 |
+
"peerDependenciesMeta": {
|
| 1643 |
+
"@types/node": {
|
| 1644 |
+
"optional": true
|
| 1645 |
+
},
|
| 1646 |
+
"less": {
|
| 1647 |
+
"optional": true
|
| 1648 |
+
},
|
| 1649 |
+
"lightningcss": {
|
| 1650 |
+
"optional": true
|
| 1651 |
+
},
|
| 1652 |
+
"sass": {
|
| 1653 |
+
"optional": true
|
| 1654 |
+
},
|
| 1655 |
+
"sass-embedded": {
|
| 1656 |
+
"optional": true
|
| 1657 |
+
},
|
| 1658 |
+
"stylus": {
|
| 1659 |
+
"optional": true
|
| 1660 |
+
},
|
| 1661 |
+
"sugarss": {
|
| 1662 |
+
"optional": true
|
| 1663 |
+
},
|
| 1664 |
+
"terser": {
|
| 1665 |
+
"optional": true
|
| 1666 |
+
}
|
| 1667 |
+
}
|
| 1668 |
+
},
|
| 1669 |
+
"node_modules/yallist": {
|
| 1670 |
+
"version": "3.1.1",
|
| 1671 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 1672 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 1673 |
+
"dev": true,
|
| 1674 |
+
"license": "ISC"
|
| 1675 |
+
}
|
| 1676 |
+
}
|
| 1677 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "codesentinel-frontend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 17 |
+
"vite": "^5.4.2"
|
| 18 |
+
}
|
| 19 |
+
}
|
frontend/public/favicon.svg
ADDED
|
|
frontend/public/icons.svg
ADDED
|
|
frontend/src/App.css
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.counter {
|
| 2 |
+
font-size: 16px;
|
| 3 |
+
padding: 5px 10px;
|
| 4 |
+
border-radius: 5px;
|
| 5 |
+
color: var(--accent);
|
| 6 |
+
background: var(--accent-bg);
|
| 7 |
+
border: 2px solid transparent;
|
| 8 |
+
transition: border-color 0.3s;
|
| 9 |
+
margin-bottom: 24px;
|
| 10 |
+
|
| 11 |
+
&:hover {
|
| 12 |
+
border-color: var(--accent-border);
|
| 13 |
+
}
|
| 14 |
+
&:focus-visible {
|
| 15 |
+
outline: 2px solid var(--accent);
|
| 16 |
+
outline-offset: 2px;
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.hero {
|
| 21 |
+
position: relative;
|
| 22 |
+
|
| 23 |
+
.base,
|
| 24 |
+
.framework,
|
| 25 |
+
.vite {
|
| 26 |
+
inset-inline: 0;
|
| 27 |
+
margin: 0 auto;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.base {
|
| 31 |
+
width: 170px;
|
| 32 |
+
position: relative;
|
| 33 |
+
z-index: 0;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.framework,
|
| 37 |
+
.vite {
|
| 38 |
+
position: absolute;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.framework {
|
| 42 |
+
z-index: 1;
|
| 43 |
+
top: 34px;
|
| 44 |
+
height: 28px;
|
| 45 |
+
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
| 46 |
+
scale(1.4);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.vite {
|
| 50 |
+
z-index: 0;
|
| 51 |
+
top: 107px;
|
| 52 |
+
height: 26px;
|
| 53 |
+
width: auto;
|
| 54 |
+
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
| 55 |
+
scale(0.8);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
#center {
|
| 60 |
+
display: flex;
|
| 61 |
+
flex-direction: column;
|
| 62 |
+
gap: 25px;
|
| 63 |
+
place-content: center;
|
| 64 |
+
place-items: center;
|
| 65 |
+
flex-grow: 1;
|
| 66 |
+
|
| 67 |
+
@media (max-width: 1024px) {
|
| 68 |
+
padding: 32px 20px 24px;
|
| 69 |
+
gap: 18px;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#next-steps {
|
| 74 |
+
display: flex;
|
| 75 |
+
border-top: 1px solid var(--border);
|
| 76 |
+
text-align: left;
|
| 77 |
+
|
| 78 |
+
& > div {
|
| 79 |
+
flex: 1 1 0;
|
| 80 |
+
padding: 32px;
|
| 81 |
+
@media (max-width: 1024px) {
|
| 82 |
+
padding: 24px 20px;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.icon {
|
| 87 |
+
margin-bottom: 16px;
|
| 88 |
+
width: 22px;
|
| 89 |
+
height: 22px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@media (max-width: 1024px) {
|
| 93 |
+
flex-direction: column;
|
| 94 |
+
text-align: center;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
#docs {
|
| 99 |
+
border-right: 1px solid var(--border);
|
| 100 |
+
|
| 101 |
+
@media (max-width: 1024px) {
|
| 102 |
+
border-right: none;
|
| 103 |
+
border-bottom: 1px solid var(--border);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
#next-steps ul {
|
| 108 |
+
list-style: none;
|
| 109 |
+
padding: 0;
|
| 110 |
+
display: flex;
|
| 111 |
+
gap: 8px;
|
| 112 |
+
margin: 32px 0 0;
|
| 113 |
+
|
| 114 |
+
.logo {
|
| 115 |
+
height: 18px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
a {
|
| 119 |
+
color: var(--text-h);
|
| 120 |
+
font-size: 16px;
|
| 121 |
+
border-radius: 6px;
|
| 122 |
+
background: var(--social-bg);
|
| 123 |
+
display: flex;
|
| 124 |
+
padding: 6px 12px;
|
| 125 |
+
align-items: center;
|
| 126 |
+
gap: 8px;
|
| 127 |
+
text-decoration: none;
|
| 128 |
+
transition: box-shadow 0.3s;
|
| 129 |
+
|
| 130 |
+
&:hover {
|
| 131 |
+
box-shadow: var(--shadow);
|
| 132 |
+
}
|
| 133 |
+
.button-icon {
|
| 134 |
+
height: 18px;
|
| 135 |
+
width: 18px;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
@media (max-width: 1024px) {
|
| 140 |
+
margin-top: 20px;
|
| 141 |
+
flex-wrap: wrap;
|
| 142 |
+
justify-content: center;
|
| 143 |
+
|
| 144 |
+
li {
|
| 145 |
+
flex: 1 1 calc(50% - 8px);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
a {
|
| 149 |
+
width: 100%;
|
| 150 |
+
justify-content: center;
|
| 151 |
+
box-sizing: border-box;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
#spacer {
|
| 157 |
+
height: 88px;
|
| 158 |
+
border-top: 1px solid var(--border);
|
| 159 |
+
@media (max-width: 1024px) {
|
| 160 |
+
height: 48px;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.ticks {
|
| 165 |
+
position: relative;
|
| 166 |
+
width: 100%;
|
| 167 |
+
|
| 168 |
+
&::before,
|
| 169 |
+
&::after {
|
| 170 |
+
content: '';
|
| 171 |
+
position: absolute;
|
| 172 |
+
top: -4.5px;
|
| 173 |
+
border: 5px solid transparent;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
&::before {
|
| 177 |
+
left: 0;
|
| 178 |
+
border-left-color: var(--border);
|
| 179 |
+
}
|
| 180 |
+
&::after {
|
| 181 |
+
right: 0;
|
| 182 |
+
border-right-color: var(--border);
|
| 183 |
+
}
|
| 184 |
+
}
|
frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react'
|
| 2 |
+
import Navbar from './components/Navbar'
|
| 3 |
+
import AnalyzeScreen from './screens/AnalyzeScreen'
|
| 4 |
+
import BatchScreen from './screens/BatchScreen'
|
| 5 |
+
import SimilarityScreen from './screens/SimilarityScreen'
|
| 6 |
+
import DetailScreen from './screens/DetailScreen'
|
| 7 |
+
|
| 8 |
+
export default function App() {
|
| 9 |
+
const [screen, setScreen] = useState('analyze')
|
| 10 |
+
const [detailRow, setDetailRow] = useState(null)
|
| 11 |
+
const [serverOk, setServerOk] = useState(null)
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
fetch('/api/health')
|
| 15 |
+
.then(r => r.json())
|
| 16 |
+
.then(d => setServerOk(d.model_loaded))
|
| 17 |
+
.catch(() => setServerOk(false))
|
| 18 |
+
}, [])
|
| 19 |
+
|
| 20 |
+
const navigate = (s) => { setScreen(s); setDetailRow(null) }
|
| 21 |
+
const openDetail = (row) => { setDetailRow(row); setScreen('detail') }
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div style={{ minHeight: '100vh', background: '#0A0F1C', position: 'relative' }}>
|
| 25 |
+
{/* Atmospheric glow */}
|
| 26 |
+
<div style={{
|
| 27 |
+
position: 'fixed', inset: 0, pointerEvents: 'none', zIndex: 0,
|
| 28 |
+
background: 'radial-gradient(ellipse 1200px 600px at 50% -10%, rgba(0,212,255,0.08), transparent 60%)',
|
| 29 |
+
}}/>
|
| 30 |
+
|
| 31 |
+
<div style={{ position: 'relative', zIndex: 1 }}>
|
| 32 |
+
<Navbar active={screen === 'detail' ? 'batch' : screen} onNavigate={navigate}/>
|
| 33 |
+
|
| 34 |
+
{/* Server status banner */}
|
| 35 |
+
{serverOk === false && (
|
| 36 |
+
<div style={{
|
| 37 |
+
background: 'rgba(239,68,68,0.12)', borderBottom: '1px solid rgba(239,68,68,0.25)',
|
| 38 |
+
padding: '10px 80px', display: 'flex', alignItems: 'center', gap: 10, fontSize: 13, color: '#EF4444',
|
| 39 |
+
}}>
|
| 40 |
+
<span>⚠</span>
|
| 41 |
+
<span>Flask server not reachable. Start it with: <code style={{ fontFamily: 'JetBrains Mono, monospace', background: 'rgba(239,68,68,0.12)', padding: '2px 6px', borderRadius: 4 }}>python app.py</code></span>
|
| 42 |
+
</div>
|
| 43 |
+
)}
|
| 44 |
+
|
| 45 |
+
{serverOk === true && screen === 'analyze' && (
|
| 46 |
+
<div style={{
|
| 47 |
+
background: 'rgba(16,185,129,0.08)', borderBottom: '1px solid rgba(16,185,129,0.15)',
|
| 48 |
+
padding: '8px 80px', display: 'flex', alignItems: 'center', gap: 10, fontSize: 12, color: '#10B981',
|
| 49 |
+
}}>
|
| 50 |
+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#10B981', display: 'inline-block' }}/>
|
| 51 |
+
Server connected · Model loaded
|
| 52 |
+
</div>
|
| 53 |
+
)}
|
| 54 |
+
|
| 55 |
+
{screen === 'analyze' && <AnalyzeScreen/>}
|
| 56 |
+
{screen === 'batch' && <BatchScreen onOpenRow={openDetail}/>}
|
| 57 |
+
{screen === 'similarity' && <SimilarityScreen/>}
|
| 58 |
+
{screen === 'detail' && <DetailScreen row={detailRow} onBack={() => setScreen('batch')}/>}
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
)
|
| 62 |
+
}
|
frontend/src/api/client.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// api/client.js — sve API pozive na Flask backend
|
| 2 |
+
|
| 3 |
+
const BASE = '/api'
|
| 4 |
+
|
| 5 |
+
async function request(method, path, body) {
|
| 6 |
+
const res = await fetch(`${BASE}${path}`, {
|
| 7 |
+
method,
|
| 8 |
+
headers: { 'Content-Type': 'application/json' },
|
| 9 |
+
body: body ? JSON.stringify(body) : undefined,
|
| 10 |
+
})
|
| 11 |
+
if (!res.ok) {
|
| 12 |
+
const err = await res.json().catch(() => ({}))
|
| 13 |
+
throw new Error(err.error || `HTTP ${res.status}`)
|
| 14 |
+
}
|
| 15 |
+
return res.json()
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Provjera statusa servera
|
| 19 |
+
export const checkHealth = () => request('GET', '/health')
|
| 20 |
+
|
| 21 |
+
// Analiza jednog isječka koda
|
| 22 |
+
export const analyzeCode = (code, language, filename) =>
|
| 23 |
+
request('POST', '/analyze', { code, language, filename })
|
| 24 |
+
|
| 25 |
+
// Analiza više kodova odjednom
|
| 26 |
+
export const analyzeBatch = (submissions) =>
|
| 27 |
+
request('POST', '/analyze-batch', { submissions })
|
| 28 |
+
|
| 29 |
+
// Matrica sličnosti
|
| 30 |
+
export const computeSimilarity = (submissions) =>
|
| 31 |
+
request('POST', '/similarity', { submissions })
|
frontend/src/assets/hero.png
ADDED
|
frontend/src/assets/react.svg
ADDED
|
|
frontend/src/assets/vite.svg
ADDED
|
|
frontend/src/components/CodeEditor.jsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { LangBadge } from './primitives'
|
| 2 |
+
|
| 3 |
+
function SyntaxTinted({ code }) {
|
| 4 |
+
const tokens = code.split(/(\b(?:def|return|if|else|elif|for|while|import|from|class|in|not|and|or|True|False|None|self|function|const|let|var|=>|public|private|static|void|int|string|bool)\b|"[^"]*"|'[^']*'|`[^`]*`|#[^\n]*|\/\/[^\n]*|\/\*[\s\S]*?\*\/|\b\d+\b)/g)
|
| 5 |
+
return tokens.map((t, i) => {
|
| 6 |
+
if (/^(def|return|if|else|elif|for|while|import|from|class|in|not|and|or|True|False|None|self|function|const|let|var|public|private|static|void|int|string|bool)$/.test(t))
|
| 7 |
+
return <span key={i} style={{ color: '#22E0FF' }}>{t}</span>
|
| 8 |
+
if (/^["'`].*["'`]$/.test(t)) return <span key={i} style={{ color: '#10B981' }}>{t}</span>
|
| 9 |
+
if (/^(#|\/\/)/.test(t)) return <span key={i} style={{ color: '#64748B', fontStyle: 'italic' }}>{t}</span>
|
| 10 |
+
if (/^\d+$/.test(t)) return <span key={i} style={{ color: '#F59E0B' }}>{t}</span>
|
| 11 |
+
return <span key={i}>{t}</span>
|
| 12 |
+
})
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function CodeEditor({ code, onChange, lang = 'Python', readOnly = false, height = 360, annotations = [], filename }) {
|
| 16 |
+
const lines = (code || '').split('\n')
|
| 17 |
+
const displayName = filename || 'submission.' + (lang === 'Python' ? 'py' : lang === 'Java' ? 'java' : lang === 'JavaScript' ? 'js' : 'txt')
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<div style={{
|
| 21 |
+
background: '#0F172A', border: '1px solid rgba(148,163,184,0.12)',
|
| 22 |
+
borderRadius: 10, overflow: 'hidden', boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.03)',
|
| 23 |
+
}}>
|
| 24 |
+
{/* Header bar */}
|
| 25 |
+
<div style={{
|
| 26 |
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
| 27 |
+
padding: '10px 14px', borderBottom: '1px solid rgba(148,163,184,0.12)',
|
| 28 |
+
background: 'rgba(15,23,41,0.6)',
|
| 29 |
+
}}>
|
| 30 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
| 31 |
+
<div style={{ display: 'flex', gap: 6 }}>
|
| 32 |
+
<span style={{ width: 10, height: 10, borderRadius: '50%', background: '#EF4444', opacity: 0.6 }}/>
|
| 33 |
+
<span style={{ width: 10, height: 10, borderRadius: '50%', background: '#F59E0B', opacity: 0.6 }}/>
|
| 34 |
+
<span style={{ width: 10, height: 10, borderRadius: '50%', background: '#10B981', opacity: 0.6 }}/>
|
| 35 |
+
</div>
|
| 36 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 12, color: '#94A3B8' }}>{displayName}</span>
|
| 37 |
+
</div>
|
| 38 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
| 39 |
+
{!readOnly && <span style={{ fontSize: 10, color: '#64748B', letterSpacing: '0.04em', textTransform: 'uppercase' }}>Auto-detected</span>}
|
| 40 |
+
<LangBadge lang={lang}/>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
{/* Editor body */}
|
| 45 |
+
<div style={{ display: 'flex', height, position: 'relative', overflow: 'hidden' }}>
|
| 46 |
+
{/* Line numbers */}
|
| 47 |
+
<div style={{
|
| 48 |
+
padding: '12px 12px 12px 16px', background: 'rgba(10,15,28,0.4)',
|
| 49 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 12, lineHeight: 1.7,
|
| 50 |
+
color: '#475569', textAlign: 'right', userSelect: 'none',
|
| 51 |
+
borderRight: '1px solid rgba(148,163,184,0.08)', minWidth: 44,
|
| 52 |
+
overflowY: 'hidden',
|
| 53 |
+
}}>
|
| 54 |
+
{lines.map((_, i) => {
|
| 55 |
+
const ann = annotations.find(a => a.line === i + 1)
|
| 56 |
+
return (
|
| 57 |
+
<div key={i} style={{
|
| 58 |
+
color: ann ? (ann.tone === 'red' ? '#EF4444' : '#F59E0B') : '#475569',
|
| 59 |
+
fontWeight: ann ? 600 : 400,
|
| 60 |
+
}}>{i + 1}</div>
|
| 61 |
+
)
|
| 62 |
+
})}
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
{/* Code area */}
|
| 66 |
+
<div style={{ flex: 1, position: 'relative', overflow: 'auto' }}>
|
| 67 |
+
{annotations.map((a, i) => (
|
| 68 |
+
<div key={i} style={{
|
| 69 |
+
position: 'absolute', left: 0, right: 0,
|
| 70 |
+
top: `calc(12px + ${a.line - 1} * 1.7em)`, height: '1.7em',
|
| 71 |
+
background: a.tone === 'red' ? 'rgba(239,68,68,0.08)' : 'rgba(245,158,11,0.08)',
|
| 72 |
+
borderLeft: `2px solid ${a.tone === 'red' ? '#EF4444' : '#F59E0B'}`,
|
| 73 |
+
pointerEvents: 'none', zIndex: 1,
|
| 74 |
+
}}>
|
| 75 |
+
{a.note && (
|
| 76 |
+
<span style={{
|
| 77 |
+
position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)',
|
| 78 |
+
fontSize: 10, color: a.tone === 'red' ? '#EF4444' : '#F59E0B',
|
| 79 |
+
fontFamily: 'Inter, sans-serif', background: a.tone === 'red' ? 'rgba(239,68,68,0.12)' : 'rgba(245,158,11,0.12)',
|
| 80 |
+
padding: '2px 6px', borderRadius: 4,
|
| 81 |
+
}}>⚠ {a.note}</span>
|
| 82 |
+
)}
|
| 83 |
+
</div>
|
| 84 |
+
))}
|
| 85 |
+
{readOnly ? (
|
| 86 |
+
<pre style={{
|
| 87 |
+
margin: 0, padding: '12px 16px',
|
| 88 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 12, lineHeight: 1.7,
|
| 89 |
+
color: '#CBD5E1', whiteSpace: 'pre', overflow: 'visible',
|
| 90 |
+
}}><SyntaxTinted code={code}/></pre>
|
| 91 |
+
) : (
|
| 92 |
+
<textarea value={code} onChange={e => onChange && onChange(e.target.value)}
|
| 93 |
+
spellCheck="false" placeholder="Paste your code here…"
|
| 94 |
+
style={{
|
| 95 |
+
width: '100%', height: '100%', border: 'none', outline: 'none', resize: 'none',
|
| 96 |
+
background: 'transparent', color: '#CBD5E1',
|
| 97 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 12, lineHeight: 1.7,
|
| 98 |
+
padding: '12px 16px',
|
| 99 |
+
}}/>
|
| 100 |
+
)}
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
)
|
| 105 |
+
}
|
frontend/src/components/DetailSidebar.jsx
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Card, Label, Badge, Icon } from './primitives'
|
| 2 |
+
|
| 3 |
+
function SuspiciousPair({ a, b, similarity }) {
|
| 4 |
+
const color = similarity >= 85 ? '#EF4444' : '#F59E0B'
|
| 5 |
+
return (
|
| 6 |
+
<Card hoverable style={{ padding: '16px 20px' }}>
|
| 7 |
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
| 8 |
+
<div>
|
| 9 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
|
| 10 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 14, fontWeight: 600, color: '#F8FAFC' }}>{a}</span>
|
| 11 |
+
<Icon name="chevronRight" size={14} color="#64748B"/>
|
| 12 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 14, fontWeight: 600, color: '#F8FAFC' }}>{b}</span>
|
| 13 |
+
</div>
|
| 14 |
+
<div style={{ fontSize: 12, color: '#64748B' }}>High structural similarity detected</div>
|
| 15 |
+
</div>
|
| 16 |
+
<div style={{ textAlign: 'right' }}>
|
| 17 |
+
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 28, fontWeight: 700, color, lineHeight: 1 }}>
|
| 18 |
+
{similarity}<span style={{ fontSize: 14 }}>%</span>
|
| 19 |
+
</div>
|
| 20 |
+
<div style={{ fontSize: 10, color: '#64748B', letterSpacing: '0.04em', textTransform: 'uppercase', marginTop: 4 }}>Similar</div>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</Card>
|
| 24 |
+
)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export { SuspiciousPair }
|
| 28 |
+
|
| 29 |
+
export default function DetailSidebar({ features }) {
|
| 30 |
+
const groups = features.reduce((acc, f) => {
|
| 31 |
+
const cat = f.category || 'Other';
|
| 32 |
+
(acc[cat] = acc[cat] || []).push(f)
|
| 33 |
+
return acc
|
| 34 |
+
}, {})
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
| 38 |
+
{Object.entries(groups).map(([cat, items]) => (
|
| 39 |
+
<Card key={cat} style={{ padding: 18 }}>
|
| 40 |
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
|
| 41 |
+
<Label style={{ marginBottom: 0 }}>{cat}</Label>
|
| 42 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11, color: '#64748B' }}>{items.length}</span>
|
| 43 |
+
</div>
|
| 44 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
| 45 |
+
{items.map((f, i) => {
|
| 46 |
+
const color = f.severity === 'high' ? '#EF4444' : f.severity === 'medium' ? '#F59E0B' : '#10B981'
|
| 47 |
+
const pct = f.severity === 'high' ? 85 : f.severity === 'medium' ? 55 : 22
|
| 48 |
+
return (
|
| 49 |
+
<div key={i}>
|
| 50 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
|
| 51 |
+
<span style={{ fontSize: 12, color: '#CBD5E1' }}>{f.name}</span>
|
| 52 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 12, fontWeight: 600, color }}>{f.value}</span>
|
| 53 |
+
</div>
|
| 54 |
+
<div style={{ height: 3, background: 'rgba(148,163,184,0.10)', borderRadius: 2, overflow: 'hidden' }}>
|
| 55 |
+
<div style={{ width: `${pct}%`, height: '100%', background: color }}/>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
)
|
| 59 |
+
})}
|
| 60 |
+
</div>
|
| 61 |
+
</Card>
|
| 62 |
+
))}
|
| 63 |
+
</div>
|
| 64 |
+
)
|
| 65 |
+
}
|
frontend/src/components/Dropzone.jsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { Icon } from './primitives'
|
| 3 |
+
|
| 4 |
+
export default function Dropzone({ files = [], onUpload }) {
|
| 5 |
+
const [dragOver, setDragOver] = useState(false)
|
| 6 |
+
return (
|
| 7 |
+
<div
|
| 8 |
+
onDragOver={e => { e.preventDefault(); setDragOver(true) }}
|
| 9 |
+
onDragLeave={() => setDragOver(false)}
|
| 10 |
+
onDrop={e => { e.preventDefault(); setDragOver(false); onUpload && onUpload(e.dataTransfer.files) }}
|
| 11 |
+
onClick={() => { const i = document.createElement('input'); i.type = 'file'; i.multiple = true; i.accept = '.py,.js,.java,.cpp,.ts,.go,.rs,.rb,.c,.h'; i.onchange = e => onUpload && onUpload(e.target.files); i.click() }}
|
| 12 |
+
style={{
|
| 13 |
+
border: `1.5px dashed ${dragOver ? 'rgba(0,212,255,0.6)' : 'rgba(148,163,184,0.25)'}`,
|
| 14 |
+
borderRadius: 12, padding: '48px 24px', textAlign: 'center',
|
| 15 |
+
background: dragOver ? 'rgba(0,212,255,0.04)' : 'rgba(17,24,39,0.5)',
|
| 16 |
+
cursor: 'pointer', transition: 'all .12s cubic-bezier(0.22,1,0.36,1)',
|
| 17 |
+
backdropFilter: 'blur(20px)',
|
| 18 |
+
}}>
|
| 19 |
+
<div style={{
|
| 20 |
+
width: 56, height: 56, margin: '0 auto 18px', borderRadius: 12,
|
| 21 |
+
background: 'rgba(0,212,255,0.10)', border: '1px solid rgba(0,212,255,0.25)',
|
| 22 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 23 |
+
}}>
|
| 24 |
+
<Icon name="upload" size={24} color="#00D4FF"/>
|
| 25 |
+
</div>
|
| 26 |
+
<div style={{ fontSize: 18, fontWeight: 600, color: '#F8FAFC', marginBottom: 6 }}>
|
| 27 |
+
{files.length > 0 ? `${files.length} files ready to analyze` : 'Drop student files here or click to upload'}
|
| 28 |
+
</div>
|
| 29 |
+
<div style={{ fontSize: 13, color: '#64748B' }}>
|
| 30 |
+
Accepts <span style={{ fontFamily: 'JetBrains Mono, monospace', color: '#94A3B8' }}>.py .js .java .cpp .ts .go .rs</span> · max 200 files
|
| 31 |
+
</div>
|
| 32 |
+
{files.length > 0 && (
|
| 33 |
+
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'center', gap: 6, flexWrap: 'wrap', maxWidth: 600, marginLeft: 'auto', marginRight: 'auto' }}>
|
| 34 |
+
{files.slice(0, 10).map((f, i) => (
|
| 35 |
+
<span key={i} style={{
|
| 36 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 11,
|
| 37 |
+
padding: '4px 10px', background: 'rgba(0,212,255,0.08)',
|
| 38 |
+
border: '1px solid rgba(0,212,255,0.20)', borderRadius: 6, color: '#00D4FF',
|
| 39 |
+
}}>{f.name || f}</span>
|
| 40 |
+
))}
|
| 41 |
+
{files.length > 10 && <span style={{ fontSize: 11, color: '#64748B', alignSelf: 'center' }}>+{files.length - 10} more</span>}
|
| 42 |
+
</div>
|
| 43 |
+
)}
|
| 44 |
+
</div>
|
| 45 |
+
)
|
| 46 |
+
}
|
frontend/src/components/ExplanationPanel.jsx
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ExplanationPanel.jsx — prikaz objašnjenja zašto je kod klasificiran ovako
|
| 2 |
+
|
| 3 |
+
import { Card, Label, Icon } from './primitives'
|
| 4 |
+
|
| 5 |
+
const SEVERITY_CONFIG = {
|
| 6 |
+
high: {
|
| 7 |
+
color: '#EF4444',
|
| 8 |
+
bg: 'rgba(239,68,68,0.08)',
|
| 9 |
+
border: 'rgba(239,68,68,0.25)',
|
| 10 |
+
icon: 'xCircle',
|
| 11 |
+
label: 'Strong signal',
|
| 12 |
+
},
|
| 13 |
+
medium: {
|
| 14 |
+
color: '#F59E0B',
|
| 15 |
+
bg: 'rgba(245,158,11,0.08)',
|
| 16 |
+
border: 'rgba(245,158,11,0.25)',
|
| 17 |
+
icon: 'alert',
|
| 18 |
+
label: 'Moderate signal',
|
| 19 |
+
},
|
| 20 |
+
low: {
|
| 21 |
+
color: '#94A3B8',
|
| 22 |
+
bg: 'rgba(148,163,184,0.06)',
|
| 23 |
+
border: 'rgba(148,163,184,0.15)',
|
| 24 |
+
icon: 'filter',
|
| 25 |
+
label: 'Weak signal',
|
| 26 |
+
},
|
| 27 |
+
positive: {
|
| 28 |
+
color: '#10B981',
|
| 29 |
+
bg: 'rgba(16,185,129,0.08)',
|
| 30 |
+
border: 'rgba(16,185,129,0.25)',
|
| 31 |
+
icon: 'check',
|
| 32 |
+
label: 'Human indicator',
|
| 33 |
+
},
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export default function ExplanationPanel({ explanations, verdict, aiProbability }) {
|
| 37 |
+
if (!explanations || explanations.length === 0) return null
|
| 38 |
+
|
| 39 |
+
const highCount = explanations.filter(e => e.severity === 'high').length
|
| 40 |
+
const medCount = explanations.filter(e => e.severity === 'medium').length
|
| 41 |
+
const posCount = explanations.filter(e => e.severity === 'positive').length
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
<Card style={{ padding: 24, marginTop: 24 }}>
|
| 45 |
+
{/* Header */}
|
| 46 |
+
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 20 }}>
|
| 47 |
+
<div>
|
| 48 |
+
<Label style={{ marginBottom: 6 }}>Analysis reasoning</Label>
|
| 49 |
+
<p style={{ margin: 0, fontSize: 13, color: '#64748B', lineHeight: 1.5, maxWidth: 560 }}>
|
| 50 |
+
The following signals contributed to the classification. Strong signals carry the most weight;
|
| 51 |
+
human indicators suggest characteristics inconsistent with AI generation.
|
| 52 |
+
</p>
|
| 53 |
+
</div>
|
| 54 |
+
{/* Signal summary pills */}
|
| 55 |
+
<div style={{ display: 'flex', gap: 8, flexShrink: 0, marginLeft: 24 }}>
|
| 56 |
+
{highCount > 0 && (
|
| 57 |
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 999, fontSize: 11, fontWeight: 600, background: 'rgba(239,68,68,0.12)', border: '1px solid rgba(239,68,68,0.30)', color: '#EF4444' }}>
|
| 58 |
+
<span style={{ width: 5, height: 5, borderRadius: '50%', background: '#EF4444' }}/>
|
| 59 |
+
{highCount} strong
|
| 60 |
+
</span>
|
| 61 |
+
)}
|
| 62 |
+
{medCount > 0 && (
|
| 63 |
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 999, fontSize: 11, fontWeight: 600, background: 'rgba(245,158,11,0.12)', border: '1px solid rgba(245,158,11,0.30)', color: '#F59E0B' }}>
|
| 64 |
+
<span style={{ width: 5, height: 5, borderRadius: '50%', background: '#F59E0B' }}/>
|
| 65 |
+
{medCount} moderate
|
| 66 |
+
</span>
|
| 67 |
+
)}
|
| 68 |
+
{posCount > 0 && (
|
| 69 |
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 999, fontSize: 11, fontWeight: 600, background: 'rgba(16,185,129,0.12)', border: '1px solid rgba(16,185,129,0.30)', color: '#10B981' }}>
|
| 70 |
+
<span style={{ width: 5, height: 5, borderRadius: '50%', background: '#10B981' }}/>
|
| 71 |
+
{posCount} human
|
| 72 |
+
</span>
|
| 73 |
+
)}
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
{/* Explanation items */}
|
| 78 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
| 79 |
+
{explanations.map((exp, i) => {
|
| 80 |
+
const cfg = SEVERITY_CONFIG[exp.severity] || SEVERITY_CONFIG.low
|
| 81 |
+
return (
|
| 82 |
+
<div key={i} style={{
|
| 83 |
+
display: 'flex', gap: 14, alignItems: 'flex-start',
|
| 84 |
+
padding: '14px 16px',
|
| 85 |
+
background: cfg.bg,
|
| 86 |
+
border: `1px solid ${cfg.border}`,
|
| 87 |
+
borderRadius: 10,
|
| 88 |
+
animation: `fadeIn .25s ease ${i * 0.04}s both`,
|
| 89 |
+
}}>
|
| 90 |
+
{/* Icon */}
|
| 91 |
+
<div style={{
|
| 92 |
+
flexShrink: 0, width: 28, height: 28, borderRadius: 8,
|
| 93 |
+
background: `${cfg.color}18`,
|
| 94 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 1,
|
| 95 |
+
}}>
|
| 96 |
+
<Icon name={cfg.icon} size={14} color={cfg.color}/>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
{/* Text */}
|
| 100 |
+
<div style={{ flex: 1, minWidth: 0 }}>
|
| 101 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 5 }}>
|
| 102 |
+
<span style={{ fontSize: 10, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', color: cfg.color }}>
|
| 103 |
+
{cfg.label}
|
| 104 |
+
</span>
|
| 105 |
+
<span style={{ fontSize: 10, color: '#475569', fontFamily: 'JetBrains Mono, monospace' }}>
|
| 106 |
+
{exp.feature.replace(/_/g, ' ')}
|
| 107 |
+
</span>
|
| 108 |
+
</div>
|
| 109 |
+
<p style={{ margin: 0, fontSize: 13, color: '#CBD5E1', lineHeight: 1.65 }}>
|
| 110 |
+
{exp.text}
|
| 111 |
+
</p>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
)
|
| 115 |
+
})}
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
{/* Footer disclaimer */}
|
| 119 |
+
<div style={{
|
| 120 |
+
marginTop: 16, paddingTop: 16,
|
| 121 |
+
borderTop: '1px solid rgba(148,163,184,0.10)',
|
| 122 |
+
fontSize: 12, color: '#475569', lineHeight: 1.55, fontStyle: 'italic',
|
| 123 |
+
}}>
|
| 124 |
+
CodeSentinel reports statistical signals, not academic-integrity verdicts.
|
| 125 |
+
A high AI probability score should be treated as a starting point for review,
|
| 126 |
+
not as conclusive evidence. Confirm findings with the student before taking any action.
|
| 127 |
+
</div>
|
| 128 |
+
</Card>
|
| 129 |
+
)
|
| 130 |
+
}
|
frontend/src/components/FeatureCard.jsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Card, Badge, Icon, Label } from './primitives'
|
| 2 |
+
|
| 3 |
+
export function FeatureCard({ name, value, severity = 'low', unit = '', icon }) {
|
| 4 |
+
const tones = {
|
| 5 |
+
high: { color: '#EF4444', label: 'HIGH', tone: 'red', pct: 85 },
|
| 6 |
+
medium: { color: '#F59E0B', label: 'MEDIUM', tone: 'amber', pct: 55 },
|
| 7 |
+
low: { color: '#10B981', label: 'LOW', tone: 'green', pct: 22 },
|
| 8 |
+
}
|
| 9 |
+
const t = tones[severity] || tones.low
|
| 10 |
+
return (
|
| 11 |
+
<Card hoverable>
|
| 12 |
+
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 12 }}>
|
| 13 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#94A3B8' }}>
|
| 14 |
+
{icon && <Icon name={icon} size={14}/>}
|
| 15 |
+
<Label style={{ marginBottom: 0 }}>{name}</Label>
|
| 16 |
+
</div>
|
| 17 |
+
<Badge tone={t.tone} size="sm">{t.label}</Badge>
|
| 18 |
+
</div>
|
| 19 |
+
<div style={{
|
| 20 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 28, fontWeight: 700,
|
| 21 |
+
color: '#F8FAFC', lineHeight: 1, fontVariantNumeric: 'tabular-nums',
|
| 22 |
+
}}>{value}<span style={{ fontSize: 14, color: '#64748B', marginLeft: 4 }}>{unit}</span></div>
|
| 23 |
+
<div style={{ height: 4, background: 'rgba(148,163,184,0.10)', borderRadius: 2, marginTop: 14, overflow: 'hidden' }}>
|
| 24 |
+
<div style={{ width: `${t.pct}%`, height: '100%', background: t.color, borderRadius: 2, transition: 'width .6s cubic-bezier(0.22,1,0.36,1)' }}/>
|
| 25 |
+
</div>
|
| 26 |
+
</Card>
|
| 27 |
+
)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function StatCard({ label, value, delta, deltaColor = '#94A3B8', icon, iconColor = '#00D4FF' }) {
|
| 31 |
+
return (
|
| 32 |
+
<Card>
|
| 33 |
+
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 12 }}>
|
| 34 |
+
<Label style={{ marginBottom: 0 }}>{label}</Label>
|
| 35 |
+
<div style={{
|
| 36 |
+
width: 32, height: 32, borderRadius: 8,
|
| 37 |
+
background: `${iconColor}15`, border: `1px solid ${iconColor}30`,
|
| 38 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 39 |
+
}}>
|
| 40 |
+
<Icon name={icon} size={16} color={iconColor}/>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div style={{
|
| 44 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 32, fontWeight: 700,
|
| 45 |
+
color: '#F8FAFC', lineHeight: 1, fontVariantNumeric: 'tabular-nums',
|
| 46 |
+
}}>{value}</div>
|
| 47 |
+
{delta && <div style={{ fontSize: 12, color: deltaColor, marginTop: 8 }}>{delta}</div>}
|
| 48 |
+
</Card>
|
| 49 |
+
)
|
| 50 |
+
}
|
frontend/src/components/Heatmap.jsx
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
|
| 3 |
+
// Pretvara vrijednost 0-100 u boju:
|
| 4 |
+
// tamno plava (0%) → cyan (50%) → amber (75%) → crvena (100%)
|
| 5 |
+
function colorFor(v) {
|
| 6 |
+
if (v == null) return '#0F1729'
|
| 7 |
+
if (v < 30) {
|
| 8 |
+
const t = v / 30
|
| 9 |
+
return `rgb(${Math.round(15 + t * 10)}, ${Math.round(28 + t * 40)}, ${Math.round(60 + t * 80)})`
|
| 10 |
+
}
|
| 11 |
+
if (v < 60) {
|
| 12 |
+
const t = (v - 30) / 30
|
| 13 |
+
return `rgb(${Math.round(25 + t * (0 - 25))}, ${Math.round(68 + t * (212 - 68))}, ${Math.round(140 + t * (255 - 140))})`
|
| 14 |
+
}
|
| 15 |
+
if (v < 80) {
|
| 16 |
+
const t = (v - 60) / 20
|
| 17 |
+
return `rgb(${Math.round(t * 245)}, ${Math.round(212 + t * (158 - 212))}, ${Math.round(255 + t * (11 - 255))})`
|
| 18 |
+
}
|
| 19 |
+
const t = (v - 80) / 20
|
| 20 |
+
return `rgb(${Math.round(245 + t * (239 - 245))}, ${Math.round(158 + t * (68 - 158))}, ${Math.round(11 + t * (68 - 11))})`
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export default function Heatmap({ data, ids }) {
|
| 24 |
+
const [tip, setTip] = useState(null)
|
| 25 |
+
|
| 26 |
+
if (!data || !ids || ids.length === 0) {
|
| 27 |
+
return (
|
| 28 |
+
<div style={{ padding: 40, textAlign: 'center', color: '#64748B', fontSize: 14 }}>
|
| 29 |
+
No similarity data available.
|
| 30 |
+
</div>
|
| 31 |
+
)
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const n = ids.length
|
| 35 |
+
|
| 36 |
+
// Veličina ćelije: veća za manji broj fajlova, manja za veći broj
|
| 37 |
+
// Min 20px, max 80px — tako da uvijek bude vidljivo
|
| 38 |
+
const cellSize = Math.max(20, Math.min(80, Math.floor(480 / n)))
|
| 39 |
+
const labelW = 72
|
| 40 |
+
const showPct = cellSize >= 36 // prikaži postotak u ćeliji ako ima mjesta
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div style={{ position: 'relative' }}>
|
| 44 |
+
|
| 45 |
+
{/* Upozorenje za mali broj fajlova */}
|
| 46 |
+
{n < 3 && (
|
| 47 |
+
<div style={{
|
| 48 |
+
marginBottom: 16, padding: '10px 14px',
|
| 49 |
+
background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.25)',
|
| 50 |
+
borderRadius: 8, fontSize: 12, color: '#F59E0B',
|
| 51 |
+
}}>
|
| 52 |
+
Heatmap je najkorisniji s 3 ili više datoteka. Trenutno prikazuje {n} datoteke.
|
| 53 |
+
</div>
|
| 54 |
+
)}
|
| 55 |
+
|
| 56 |
+
<div style={{ overflowX: 'auto', overflowY: 'auto' }}>
|
| 57 |
+
{/* Gornji labeli (kosi tekst) */}
|
| 58 |
+
<div style={{ display: 'flex', marginLeft: labelW }}>
|
| 59 |
+
{ids.map((id, i) => (
|
| 60 |
+
<div key={i} style={{
|
| 61 |
+
width: cellSize, flexShrink: 0,
|
| 62 |
+
height: labelW,
|
| 63 |
+
display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
|
| 64 |
+
paddingBottom: 8,
|
| 65 |
+
}}>
|
| 66 |
+
<span style={{
|
| 67 |
+
fontFamily: 'JetBrains Mono, monospace',
|
| 68 |
+
fontSize: Math.max(8, Math.min(11, cellSize * 0.35)),
|
| 69 |
+
color: '#94A3B8',
|
| 70 |
+
transform: 'rotate(-45deg)',
|
| 71 |
+
transformOrigin: 'bottom center',
|
| 72 |
+
whiteSpace: 'nowrap',
|
| 73 |
+
display: 'block',
|
| 74 |
+
}}>{id}</span>
|
| 75 |
+
</div>
|
| 76 |
+
))}
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
{/* Grid s lijeve strane labelima */}
|
| 80 |
+
<div style={{ display: 'flex' }}>
|
| 81 |
+
|
| 82 |
+
{/* Lijevi labeli */}
|
| 83 |
+
<div style={{ display: 'flex', flexDirection: 'column', width: labelW, flexShrink: 0 }}>
|
| 84 |
+
{ids.map((id, i) => (
|
| 85 |
+
<div key={i} style={{
|
| 86 |
+
height: cellSize, flexShrink: 0,
|
| 87 |
+
display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
|
| 88 |
+
paddingRight: 10,
|
| 89 |
+
fontFamily: 'JetBrains Mono, monospace',
|
| 90 |
+
fontSize: Math.max(8, Math.min(11, cellSize * 0.35)),
|
| 91 |
+
color: '#94A3B8',
|
| 92 |
+
whiteSpace: 'nowrap',
|
| 93 |
+
}}>{id}</div>
|
| 94 |
+
))}
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
{/* Matrica ćelija */}
|
| 98 |
+
<div style={{
|
| 99 |
+
display: 'flex', flexDirection: 'column',
|
| 100 |
+
border: '1px solid rgba(148,163,184,0.15)',
|
| 101 |
+
borderRadius: 6, overflow: 'hidden',
|
| 102 |
+
}}>
|
| 103 |
+
{data.map((row, i) => (
|
| 104 |
+
<div key={i} style={{ display: 'flex' }}>
|
| 105 |
+
{row.map((v, j) => {
|
| 106 |
+
// Backend vraća 0.0–1.0, colorFor treba 0–100
|
| 107 |
+
const pct = Math.round(v * 100)
|
| 108 |
+
const isDiag = i === j
|
| 109 |
+
const bg = isDiag ? '#1F2937' : colorFor(pct)
|
| 110 |
+
|
| 111 |
+
// Boja teksta unutar ćelije — tamna za svijetle boje, bijela za tamne
|
| 112 |
+
const textColor = pct > 45 && pct < 75 ? '#0A0F1C' : '#FFFFFF'
|
| 113 |
+
|
| 114 |
+
return (
|
| 115 |
+
<div key={j}
|
| 116 |
+
onMouseEnter={e => setTip({ x: e.clientX, y: e.clientY, a: ids[i], b: ids[j], pct, isDiag })}
|
| 117 |
+
onMouseMove={e => setTip({ x: e.clientX, y: e.clientY, a: ids[i], b: ids[j], pct, isDiag })}
|
| 118 |
+
onMouseLeave={() => setTip(null)}
|
| 119 |
+
style={{
|
| 120 |
+
width: cellSize, height: cellSize, flexShrink: 0,
|
| 121 |
+
background: bg,
|
| 122 |
+
border: '1px solid rgba(10,15,28,0.3)',
|
| 123 |
+
cursor: isDiag ? 'default' : 'pointer',
|
| 124 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 125 |
+
transition: 'filter .1s',
|
| 126 |
+
position: 'relative',
|
| 127 |
+
}}>
|
| 128 |
+
{/* Postotak unutar ćelije (samo ako ima mjesta i nije dijagonala) */}
|
| 129 |
+
{showPct && !isDiag && (
|
| 130 |
+
<span style={{
|
| 131 |
+
fontFamily: 'JetBrains Mono, monospace',
|
| 132 |
+
fontSize: Math.max(8, cellSize * 0.28),
|
| 133 |
+
fontWeight: 700,
|
| 134 |
+
color: textColor,
|
| 135 |
+
userSelect: 'none',
|
| 136 |
+
lineHeight: 1,
|
| 137 |
+
}}>{pct}%</span>
|
| 138 |
+
)}
|
| 139 |
+
{/* Dijagonala — prikaži "—" */}
|
| 140 |
+
{isDiag && showPct && (
|
| 141 |
+
<span style={{ fontSize: cellSize * 0.3, color: '#475569' }}>—</span>
|
| 142 |
+
)}
|
| 143 |
+
</div>
|
| 144 |
+
)
|
| 145 |
+
})}
|
| 146 |
+
</div>
|
| 147 |
+
))}
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
{/* Legenda */}
|
| 153 |
+
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', gap: 12 }}>
|
| 154 |
+
<span style={{ fontSize: 11, color: '#94A3B8', letterSpacing: '0.04em', textTransform: 'uppercase' }}>Similarity</span>
|
| 155 |
+
<div style={{
|
| 156 |
+
display: 'flex', height: 10, width: 200, borderRadius: 4,
|
| 157 |
+
overflow: 'hidden', border: '1px solid rgba(148,163,184,0.10)',
|
| 158 |
+
}}>
|
| 159 |
+
{Array.from({ length: 40 }).map((_, i) => (
|
| 160 |
+
<div key={i} style={{ flex: 1, background: colorFor(i * 2.5 + 1) }}/>
|
| 161 |
+
))}
|
| 162 |
+
</div>
|
| 163 |
+
<div style={{ display: 'flex', gap: 16, fontSize: 10, fontFamily: 'JetBrains Mono, monospace' }}>
|
| 164 |
+
<span style={{ color: '#64748B' }}>0%</span>
|
| 165 |
+
<span style={{ color: '#00D4FF' }}>50%</span>
|
| 166 |
+
<span style={{ color: '#F59E0B' }}>75%</span>
|
| 167 |
+
<span style={{ color: '#EF4444' }}>100%</span>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
{/* Tooltip */}
|
| 172 |
+
{tip && !tip.isDiag && (
|
| 173 |
+
<div style={{
|
| 174 |
+
position: 'fixed', left: tip.x + 14, top: tip.y + 14,
|
| 175 |
+
background: 'rgba(10,15,28,0.97)', border: '1px solid rgba(0,212,255,0.4)',
|
| 176 |
+
padding: '10px 14px', borderRadius: 8, zIndex: 200,
|
| 177 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 11, color: '#F8FAFC',
|
| 178 |
+
boxShadow: '0 8px 24px rgba(0,0,0,0.5)', pointerEvents: 'none',
|
| 179 |
+
}}>
|
| 180 |
+
<div style={{ color: '#94A3B8', fontSize: 10, marginBottom: 4 }}>
|
| 181 |
+
{tip.a} ↔ {tip.b}
|
| 182 |
+
</div>
|
| 183 |
+
<div style={{
|
| 184 |
+
fontSize: 20, fontWeight: 700,
|
| 185 |
+
color: colorFor(tip.pct),
|
| 186 |
+
lineHeight: 1,
|
| 187 |
+
}}>
|
| 188 |
+
{tip.pct}%
|
| 189 |
+
</div>
|
| 190 |
+
<div style={{ fontSize: 10, color: '#64748B', marginTop: 3 }}>
|
| 191 |
+
{tip.pct >= 80 ? 'Very high similarity' :
|
| 192 |
+
tip.pct >= 60 ? 'High similarity' :
|
| 193 |
+
tip.pct >= 40 ? 'Moderate similarity' :
|
| 194 |
+
'Low similarity'}
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
)}
|
| 198 |
+
</div>
|
| 199 |
+
)
|
| 200 |
+
}
|
frontend/src/components/Navbar.jsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { Button } from './primitives'
|
| 3 |
+
|
| 4 |
+
function NavLink({ active, onClick, children }) {
|
| 5 |
+
const [hover, setHover] = useState(false)
|
| 6 |
+
return (
|
| 7 |
+
<button onClick={onClick}
|
| 8 |
+
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
| 9 |
+
style={{
|
| 10 |
+
background: active ? 'rgba(0,212,255,0.08)' : 'transparent',
|
| 11 |
+
border: 'none', padding: '8px 14px', borderRadius: 8,
|
| 12 |
+
fontFamily: 'Inter, sans-serif', fontSize: 14,
|
| 13 |
+
fontWeight: active ? 600 : 500,
|
| 14 |
+
color: active ? '#00D4FF' : hover ? '#F8FAFC' : '#CBD5E1',
|
| 15 |
+
cursor: 'pointer', transition: 'all .12s cubic-bezier(0.22,1,0.36,1)',
|
| 16 |
+
}}>
|
| 17 |
+
{children}
|
| 18 |
+
</button>
|
| 19 |
+
)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default function Navbar({ active, onNavigate }) {
|
| 23 |
+
const tabs = [
|
| 24 |
+
{ id: 'analyze', label: 'Analyze' },
|
| 25 |
+
{ id: 'batch', label: 'Batch Upload' },
|
| 26 |
+
{ id: 'similarity', label: 'Similarity' },
|
| 27 |
+
]
|
| 28 |
+
return (
|
| 29 |
+
<header style={{
|
| 30 |
+
position: 'sticky', top: 0, zIndex: 50, height: 72,
|
| 31 |
+
background: 'rgba(10,15,28,0.80)',
|
| 32 |
+
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
|
| 33 |
+
borderBottom: '1px solid rgba(148,163,184,0.12)',
|
| 34 |
+
}}>
|
| 35 |
+
<div style={{
|
| 36 |
+
maxWidth: 1280, height: '100%', margin: '0 auto',
|
| 37 |
+
padding: '0 80px',
|
| 38 |
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
| 39 |
+
}}>
|
| 40 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer' }}
|
| 41 |
+
onClick={() => onNavigate('analyze')}>
|
| 42 |
+
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
|
| 43 |
+
<path d="M16 1L2 6v10c0 8.5 5.8 16 14 18 8.2-2 14-9.5 14-18V6L16 1z" fill="#111827" stroke="#00D4FF" strokeWidth="1.5"/>
|
| 44 |
+
<path d="M10 16l4 4 8-8" stroke="#00D4FF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
| 45 |
+
</svg>
|
| 46 |
+
<div style={{ fontSize: 18, fontWeight: 700, letterSpacing: '-0.02em', color: '#F8FAFC' }}>
|
| 47 |
+
Code<span style={{ color: '#00D4FF' }}>Sentinel</span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
<nav style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
| 51 |
+
{tabs.map(t => (
|
| 52 |
+
<NavLink key={t.id} active={active === t.id} onClick={() => onNavigate(t.id)}>
|
| 53 |
+
{t.label}
|
| 54 |
+
</NavLink>
|
| 55 |
+
))}
|
| 56 |
+
</nav>
|
| 57 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
| 58 |
+
<div style={{
|
| 59 |
+
width: 32, height: 32, borderRadius: '50%',
|
| 60 |
+
background: 'linear-gradient(135deg, #00D4FF, #06B6D4)',
|
| 61 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 62 |
+
color: '#0A0F1C', fontWeight: 700, fontSize: 12,
|
| 63 |
+
}}>CS</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</header>
|
| 67 |
+
)
|
| 68 |
+
}
|
frontend/src/components/ResultGauge.jsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react'
|
| 2 |
+
import { Badge } from './primitives'
|
| 3 |
+
|
| 4 |
+
export default function ResultGauge({ value, size = 200, animate = true }) {
|
| 5 |
+
const [display, setDisplay] = useState(animate ? 0 : value)
|
| 6 |
+
|
| 7 |
+
useEffect(() => {
|
| 8 |
+
if (!animate) { setDisplay(value); return }
|
| 9 |
+
let raf, start
|
| 10 |
+
const dur = 800
|
| 11 |
+
const tick = (t) => {
|
| 12 |
+
if (!start) start = t
|
| 13 |
+
const p = Math.min((t - start) / dur, 1)
|
| 14 |
+
const eased = 1 - Math.pow(1 - p, 3)
|
| 15 |
+
setDisplay(Math.round(value * eased))
|
| 16 |
+
if (p < 1) raf = requestAnimationFrame(tick)
|
| 17 |
+
}
|
| 18 |
+
raf = requestAnimationFrame(tick)
|
| 19 |
+
return () => cancelAnimationFrame(raf)
|
| 20 |
+
}, [value, animate])
|
| 21 |
+
|
| 22 |
+
const color = value >= 70 ? '#EF4444' : value >= 40 ? '#F59E0B' : '#10B981'
|
| 23 |
+
const verdict = value >= 70 ? 'LIKELY AI-GENERATED' : value >= 40 ? 'POSSIBLY AI-GENERATED' : 'LIKELY HUMAN-WRITTEN'
|
| 24 |
+
const tone = value >= 70 ? 'red' : value >= 40 ? 'amber' : 'green'
|
| 25 |
+
const r = 42, c = 2 * Math.PI * r
|
| 26 |
+
const offset = c - (display / 100) * c
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
|
| 30 |
+
<div style={{ position: 'relative', width: size, height: size }}>
|
| 31 |
+
<svg viewBox="0 0 100 100" style={{ width: '100%', height: '100%', transform: 'rotate(-90deg)' }}>
|
| 32 |
+
<circle cx="50" cy="50" r={r} fill="none" stroke="#1F2937" strokeWidth="8"/>
|
| 33 |
+
<circle cx="50" cy="50" r={r} fill="none" stroke={color} strokeWidth="8"
|
| 34 |
+
strokeDasharray={c} strokeDashoffset={offset} strokeLinecap="round"
|
| 35 |
+
style={{ transition: 'stroke-dashoffset .1s linear', filter: `drop-shadow(0 0 6px ${color}66)` }}/>
|
| 36 |
+
</svg>
|
| 37 |
+
<div style={{
|
| 38 |
+
position: 'absolute', inset: 0,
|
| 39 |
+
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
| 40 |
+
}}>
|
| 41 |
+
<div style={{
|
| 42 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: size * 0.26, fontWeight: 700,
|
| 43 |
+
color: '#F8FAFC', lineHeight: 1, fontVariantNumeric: 'tabular-nums',
|
| 44 |
+
}}>{display}<span style={{ color, fontSize: size * 0.13 }}>%</span></div>
|
| 45 |
+
<div style={{ fontSize: 10, color: '#64748B', letterSpacing: '0.08em', marginTop: 4, textTransform: 'uppercase' }}>AI probability</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
<Badge tone={tone}>{verdict}</Badge>
|
| 49 |
+
<div style={{ width: '100%', maxWidth: 240 }}>
|
| 50 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#64748B', marginBottom: 6, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
|
| 51 |
+
<span>Confidence</span>
|
| 52 |
+
<span style={{ color: '#CBD5E1', fontFamily: 'JetBrains Mono, monospace' }}>—</span>
|
| 53 |
+
</div>
|
| 54 |
+
<div style={{ height: 4, background: 'rgba(148,163,184,0.10)', borderRadius: 2, overflow: 'hidden' }}>
|
| 55 |
+
<div style={{ width: '100%', height: '100%', background: `linear-gradient(90deg, ${color}, ${color}aa)`, borderRadius: 2 }}/>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
)
|
| 60 |
+
}
|
frontend/src/components/ResultsTable.jsx
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { Badge, LangBadge, Icon } from './primitives'
|
| 3 |
+
|
| 4 |
+
function cellStyle(i, hover, first) {
|
| 5 |
+
return {
|
| 6 |
+
padding: '14px 18px', fontSize: 13,
|
| 7 |
+
borderBottom: '1px solid rgba(148,163,184,0.08)',
|
| 8 |
+
background: hover ? 'rgba(0,212,255,0.04)' : i % 2 === 1 ? '#131B2E' : 'transparent',
|
| 9 |
+
boxShadow: hover && first ? 'inset 2px 0 0 #00D4FF' : 'none',
|
| 10 |
+
transition: 'background .12s cubic-bezier(0.22,1,0.36,1)',
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function ResultsTable({ rows, onOpen }) {
|
| 15 |
+
const [sortKey, setSortKey] = useState('ai_probability')
|
| 16 |
+
const [sortDir, setSortDir] = useState('desc')
|
| 17 |
+
const [hover, setHover] = useState(null)
|
| 18 |
+
|
| 19 |
+
const sorted = [...rows].sort((a, b) => {
|
| 20 |
+
const dir = sortDir === 'asc' ? 1 : -1
|
| 21 |
+
if (a[sortKey] < b[sortKey]) return -dir
|
| 22 |
+
if (a[sortKey] > b[sortKey]) return dir
|
| 23 |
+
return 0
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
+
const toggle = (k) => {
|
| 27 |
+
if (sortKey === k) setSortDir(sortDir === 'asc' ? 'desc' : 'asc')
|
| 28 |
+
else { setSortKey(k); setSortDir('desc') }
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const Th = ({ k, children, style }) => (
|
| 32 |
+
<th onClick={() => toggle(k)} style={{
|
| 33 |
+
fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.04em',
|
| 34 |
+
fontWeight: 500, color: sortKey === k ? '#00D4FF' : '#94A3B8',
|
| 35 |
+
textAlign: 'left', padding: '14px 18px', background: '#0F1729',
|
| 36 |
+
borderBottom: '1px solid rgba(148,163,184,0.12)',
|
| 37 |
+
cursor: 'pointer', userSelect: 'none', ...style,
|
| 38 |
+
}}>
|
| 39 |
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
| 40 |
+
{children}
|
| 41 |
+
{sortKey === k && <span style={{ fontSize: 9 }}>{sortDir === 'asc' ? '▲' : '▼'}</span>}
|
| 42 |
+
</span>
|
| 43 |
+
</th>
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return (
|
| 47 |
+
<div style={{
|
| 48 |
+
background: 'rgba(17,24,39,0.6)', border: '1px solid rgba(148,163,184,0.12)',
|
| 49 |
+
borderRadius: 12, overflow: 'hidden', backdropFilter: 'blur(20px)',
|
| 50 |
+
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.03)',
|
| 51 |
+
}}>
|
| 52 |
+
<table style={{ width: '100%', borderCollapse: 'separate', borderSpacing: 0 }}>
|
| 53 |
+
<thead>
|
| 54 |
+
<tr>
|
| 55 |
+
<Th k="id">Student ID</Th>
|
| 56 |
+
<Th k="file">File</Th>
|
| 57 |
+
<Th k="detected_language">Language</Th>
|
| 58 |
+
<Th k="ai_probability">AI Probability</Th>
|
| 59 |
+
<Th k="verdict">Verdict</Th>
|
| 60 |
+
<th style={{ background: '#0F1729', borderBottom: '1px solid rgba(148,163,184,0.12)', padding: '14px 18px' }}/>
|
| 61 |
+
</tr>
|
| 62 |
+
</thead>
|
| 63 |
+
<tbody>
|
| 64 |
+
{sorted.map((r, i) => {
|
| 65 |
+
const prob = Math.round((r.ai_probability || 0) * 100)
|
| 66 |
+
const tone = prob >= 70 ? 'red' : prob >= 40 ? 'amber' : 'green'
|
| 67 |
+
const label = prob >= 70 ? 'LIKELY AI' : prob >= 40 ? 'POSSIBLY AI' : 'LIKELY HUMAN'
|
| 68 |
+
const probColor = prob >= 70 ? '#EF4444' : prob >= 40 ? '#F59E0B' : '#10B981'
|
| 69 |
+
const isHover = hover === i
|
| 70 |
+
return (
|
| 71 |
+
<tr key={r.id || i}
|
| 72 |
+
onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}
|
| 73 |
+
onClick={() => onOpen && onOpen(r)} style={{ cursor: 'pointer' }}>
|
| 74 |
+
<td style={cellStyle(i, isHover, true)}>
|
| 75 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, color: '#F8FAFC' }}>{r.id}</span>
|
| 76 |
+
</td>
|
| 77 |
+
<td style={cellStyle(i, isHover)}>
|
| 78 |
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
| 79 |
+
<Icon name="fileCode" size={14} color="#64748B"/>
|
| 80 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, color: '#CBD5E1' }}>{r.file || '—'}</span>
|
| 81 |
+
</span>
|
| 82 |
+
</td>
|
| 83 |
+
<td style={cellStyle(i, isHover)}>
|
| 84 |
+
<LangBadge lang={r.detected_language ? (r.detected_language.charAt(0).toUpperCase() + r.detected_language.slice(1)) : 'Unknown'}/>
|
| 85 |
+
</td>
|
| 86 |
+
<td style={cellStyle(i, isHover)}>
|
| 87 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
| 88 |
+
<div style={{ width: 80, height: 4, background: 'rgba(148,163,184,0.10)', borderRadius: 2, overflow: 'hidden' }}>
|
| 89 |
+
<div style={{ width: `${prob}%`, height: '100%', background: probColor }}/>
|
| 90 |
+
</div>
|
| 91 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, fontWeight: 700, color: probColor, fontVariantNumeric: 'tabular-nums', minWidth: 36 }}>{prob}%</span>
|
| 92 |
+
</div>
|
| 93 |
+
</td>
|
| 94 |
+
<td style={cellStyle(i, isHover)}><Badge tone={tone}>{label}</Badge></td>
|
| 95 |
+
<td style={cellStyle(i, isHover)}>
|
| 96 |
+
<button onClick={e => { e.stopPropagation(); onOpen && onOpen(r) }}
|
| 97 |
+
style={{
|
| 98 |
+
background: 'transparent', border: 'none', cursor: 'pointer',
|
| 99 |
+
color: isHover ? '#00D4FF' : '#64748B',
|
| 100 |
+
display: 'inline-flex', alignItems: 'center', gap: 4,
|
| 101 |
+
fontSize: 12, fontWeight: 500, padding: 6,
|
| 102 |
+
}}>
|
| 103 |
+
<Icon name="eye" size={14}/>
|
| 104 |
+
</button>
|
| 105 |
+
</td>
|
| 106 |
+
</tr>
|
| 107 |
+
)
|
| 108 |
+
})}
|
| 109 |
+
</tbody>
|
| 110 |
+
</table>
|
| 111 |
+
</div>
|
| 112 |
+
)
|
| 113 |
+
}
|
frontend/src/components/primitives.jsx
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
|
| 3 |
+
export function Icon({ name, size = 18, color, strokeWidth = 1.5, style }) {
|
| 4 |
+
const paths = {
|
| 5 |
+
shield: <><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></>,
|
| 6 |
+
code: <><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></>,
|
| 7 |
+
upload: <><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M12 12v9"/><path d="m8 17 4-5 4 5"/></>,
|
| 8 |
+
play: <polygon points="5 3 19 12 5 21 5 3"/>,
|
| 9 |
+
download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></>,
|
| 10 |
+
search: <><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></>,
|
| 11 |
+
alert: <><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><circle cx="12" cy="17" r=".5"/></>,
|
| 12 |
+
check: <><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></>,
|
| 13 |
+
filter: <><line x1="4" y1="6" x2="20" y2="6"/><line x1="6" y1="12" x2="18" y2="12"/><line x1="9" y1="18" x2="15" y2="18"/></>,
|
| 14 |
+
chevron: <polyline points="6 9 12 15 18 9"/>,
|
| 15 |
+
chevronRight: <polyline points="9 18 15 12 9 6"/>,
|
| 16 |
+
chevronLeft: <polyline points="15 18 9 12 15 6"/>,
|
| 17 |
+
file: <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></>,
|
| 18 |
+
fileCode: <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/></>,
|
| 19 |
+
users: <><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></>,
|
| 20 |
+
chart: <><path d="M3 3v18h18"/><rect x="7" y="13" width="3" height="6"/><rect x="12" y="9" width="3" height="10"/><rect x="17" y="5" width="3" height="14"/></>,
|
| 21 |
+
xCircle: <><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></>,
|
| 22 |
+
external: <><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></>,
|
| 23 |
+
eye: <><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></>,
|
| 24 |
+
sort: <><path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/></>,
|
| 25 |
+
arrowLeft: <><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></>,
|
| 26 |
+
sparkles: <><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3L12 3z"/></>,
|
| 27 |
+
x: <><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></>,
|
| 28 |
+
}
|
| 29 |
+
return (
|
| 30 |
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
| 31 |
+
stroke={color || 'currentColor'} strokeWidth={strokeWidth}
|
| 32 |
+
strokeLinecap="round" strokeLinejoin="round" style={style}>
|
| 33 |
+
{paths[name] || null}
|
| 34 |
+
</svg>
|
| 35 |
+
)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function Button({ variant = 'primary', size = 'md', icon, iconRight, children, onClick, style, disabled }) {
|
| 39 |
+
const [hover, setHover] = useState(false)
|
| 40 |
+
const [pressed, setPressed] = useState(false)
|
| 41 |
+
const base = {
|
| 42 |
+
display: 'inline-flex', alignItems: 'center', gap: 8,
|
| 43 |
+
height: size === 'sm' ? 32 : size === 'lg' ? 48 : 40,
|
| 44 |
+
padding: size === 'sm' ? '0 12px' : size === 'lg' ? '0 24px' : '0 18px',
|
| 45 |
+
borderRadius: 8, fontFamily: 'Inter, sans-serif',
|
| 46 |
+
fontSize: size === 'sm' ? 13 : size === 'lg' ? 15 : 14,
|
| 47 |
+
fontWeight: 600, cursor: disabled ? 'not-allowed' : 'pointer',
|
| 48 |
+
border: '1px solid transparent',
|
| 49 |
+
transition: 'all .12s cubic-bezier(0.22,1,0.36,1)',
|
| 50 |
+
opacity: disabled ? 0.4 : 1,
|
| 51 |
+
transform: pressed ? 'scale(0.98)' : 'scale(1)',
|
| 52 |
+
whiteSpace: 'nowrap',
|
| 53 |
+
}
|
| 54 |
+
const variants = {
|
| 55 |
+
primary: {
|
| 56 |
+
background: hover ? '#22E0FF' : '#00D4FF', color: '#0A0F1C',
|
| 57 |
+
boxShadow: pressed ? 'none' : hover ? '0 0 32px rgba(0,212,255,0.4)' : '0 0 20px rgba(0,212,255,0.2)',
|
| 58 |
+
},
|
| 59 |
+
secondary: {
|
| 60 |
+
background: 'transparent', color: hover ? '#F8FAFC' : '#CBD5E1',
|
| 61 |
+
borderColor: hover ? 'rgba(148,163,184,0.30)' : 'rgba(148,163,184,0.12)',
|
| 62 |
+
},
|
| 63 |
+
ghost: {
|
| 64 |
+
background: hover ? 'rgba(148,163,184,0.06)' : 'transparent',
|
| 65 |
+
color: hover ? '#F8FAFC' : '#94A3B8', border: 'none',
|
| 66 |
+
},
|
| 67 |
+
danger: {
|
| 68 |
+
background: hover ? 'rgba(239,68,68,0.20)' : 'rgba(239,68,68,0.12)',
|
| 69 |
+
color: '#EF4444', borderColor: 'rgba(239,68,68,0.35)',
|
| 70 |
+
},
|
| 71 |
+
}
|
| 72 |
+
return (
|
| 73 |
+
<button style={{ ...base, ...variants[variant], ...style }}
|
| 74 |
+
onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPressed(false) }}
|
| 75 |
+
onMouseDown={() => setPressed(true)} onMouseUp={() => setPressed(false)}
|
| 76 |
+
onClick={disabled ? undefined : onClick} disabled={disabled}>
|
| 77 |
+
{icon && <Icon name={icon} size={16}/>}
|
| 78 |
+
{children}
|
| 79 |
+
{iconRight && <Icon name={iconRight} size={16}/>}
|
| 80 |
+
</button>
|
| 81 |
+
)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
export function Badge({ tone = 'cyan', children, dot = true, size = 'md' }) {
|
| 85 |
+
const tones = {
|
| 86 |
+
cyan: { bg: 'rgba(0,212,255,0.12)', bd: 'rgba(0,212,255,0.30)', fg: '#00D4FF' },
|
| 87 |
+
red: { bg: 'rgba(239,68,68,0.14)', bd: 'rgba(239,68,68,0.35)', fg: '#EF4444' },
|
| 88 |
+
amber: { bg: 'rgba(245,158,11,0.14)', bd: 'rgba(245,158,11,0.35)', fg: '#F59E0B' },
|
| 89 |
+
green: { bg: 'rgba(16,185,129,0.14)', bd: 'rgba(16,185,129,0.35)', fg: '#10B981' },
|
| 90 |
+
slate: { bg: 'rgba(148,163,184,0.10)',bd: 'rgba(148,163,184,0.25)',fg: '#94A3B8' },
|
| 91 |
+
}
|
| 92 |
+
const t = tones[tone]
|
| 93 |
+
return (
|
| 94 |
+
<span style={{
|
| 95 |
+
display: 'inline-flex', alignItems: 'center', gap: 6,
|
| 96 |
+
padding: size === 'sm' ? '3px 8px' : '5px 11px',
|
| 97 |
+
borderRadius: 999, fontSize: size === 'sm' ? 10 : 11, fontWeight: 600,
|
| 98 |
+
letterSpacing: '0.04em', border: `1px solid ${t.bd}`,
|
| 99 |
+
background: t.bg, color: t.fg, lineHeight: 1, whiteSpace: 'nowrap',
|
| 100 |
+
}}>
|
| 101 |
+
{dot && <span style={{ width: 5, height: 5, borderRadius: '50%', background: t.fg }}/>}
|
| 102 |
+
{children}
|
| 103 |
+
</span>
|
| 104 |
+
)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
export function LangBadge({ lang }) {
|
| 108 |
+
const colors = { Python: '#3776AB', JavaScript: '#F7DF1E', Java: '#F89820', 'C++': '#00599C', Go: '#00ADD8', Rust: '#CE422B', Ruby: '#CC342D', TypeScript: '#3178C6' }
|
| 109 |
+
return (
|
| 110 |
+
<span style={{
|
| 111 |
+
display: 'inline-flex', alignItems: 'center', gap: 6,
|
| 112 |
+
padding: '4px 10px', borderRadius: 6,
|
| 113 |
+
fontFamily: 'JetBrains Mono, monospace', fontSize: 11, fontWeight: 500,
|
| 114 |
+
background: '#1F2937', border: '1px solid rgba(148,163,184,0.12)',
|
| 115 |
+
color: '#CBD5E1', lineHeight: 1,
|
| 116 |
+
}}>
|
| 117 |
+
<span style={{ width: 8, height: 8, borderRadius: 2, background: colors[lang] || '#94A3B8' }}/>
|
| 118 |
+
{lang || 'Unknown'}
|
| 119 |
+
</span>
|
| 120 |
+
)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
export function Card({ children, style, glow, hoverable, onClick }) {
|
| 124 |
+
const [hover, setHover] = useState(false)
|
| 125 |
+
return (
|
| 126 |
+
<div onClick={onClick}
|
| 127 |
+
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
| 128 |
+
style={{
|
| 129 |
+
background: 'rgba(17,24,39,0.6)',
|
| 130 |
+
border: `1px solid ${hover && hoverable ? 'rgba(148,163,184,0.24)' : 'rgba(148,163,184,0.12)'}`,
|
| 131 |
+
borderRadius: 12, padding: 20,
|
| 132 |
+
backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
|
| 133 |
+
boxShadow: glow
|
| 134 |
+
? 'inset 0 1px 0 rgba(255,255,255,0.03), 0 0 24px rgba(0,212,255,0.15)'
|
| 135 |
+
: 'inset 0 1px 0 rgba(255,255,255,0.03)',
|
| 136 |
+
cursor: onClick ? 'pointer' : 'default',
|
| 137 |
+
transition: 'border-color .12s cubic-bezier(0.22,1,0.36,1)',
|
| 138 |
+
...style,
|
| 139 |
+
}}>
|
| 140 |
+
{children}
|
| 141 |
+
</div>
|
| 142 |
+
)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
export function Input({ icon, placeholder, value, onChange, style, type = 'text' }) {
|
| 146 |
+
const [focus, setFocus] = useState(false)
|
| 147 |
+
return (
|
| 148 |
+
<div style={{
|
| 149 |
+
display: 'flex', alignItems: 'center', gap: 8,
|
| 150 |
+
height: 40, padding: '0 14px',
|
| 151 |
+
background: '#1F2937',
|
| 152 |
+
border: `1px solid ${focus ? 'rgba(0,212,255,0.40)' : 'rgba(148,163,184,0.12)'}`,
|
| 153 |
+
borderRadius: 8,
|
| 154 |
+
boxShadow: focus ? '0 0 0 3px rgba(0,212,255,0.10)' : 'none',
|
| 155 |
+
transition: 'all .12s cubic-bezier(0.22,1,0.36,1)',
|
| 156 |
+
...style,
|
| 157 |
+
}}>
|
| 158 |
+
{icon && <Icon name={icon} size={16} color="#94A3B8"/>}
|
| 159 |
+
<input type={type} value={value} placeholder={placeholder}
|
| 160 |
+
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
| 161 |
+
onChange={e => onChange && onChange(e.target.value)}
|
| 162 |
+
style={{
|
| 163 |
+
flex: 1, background: 'transparent', border: 'none', outline: 'none',
|
| 164 |
+
color: '#F8FAFC', fontFamily: 'Inter, sans-serif', fontSize: 14,
|
| 165 |
+
}}/>
|
| 166 |
+
</div>
|
| 167 |
+
)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
export function Label({ children, style }) {
|
| 171 |
+
return (
|
| 172 |
+
<div style={{
|
| 173 |
+
fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.04em',
|
| 174 |
+
fontWeight: 500, color: '#94A3B8', ...style,
|
| 175 |
+
}}>{children}</div>
|
| 176 |
+
)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
export function Spinner({ size = 32 }) {
|
| 180 |
+
return (
|
| 181 |
+
<div style={{
|
| 182 |
+
width: size, height: size, borderRadius: '50%',
|
| 183 |
+
border: '2px solid rgba(0,212,255,0.20)',
|
| 184 |
+
borderTopColor: '#00D4FF',
|
| 185 |
+
animation: 'spin 0.8s linear infinite',
|
| 186 |
+
}}/>
|
| 187 |
+
)
|
| 188 |
+
}
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--text: #6b6375;
|
| 3 |
+
--text-h: #08060d;
|
| 4 |
+
--bg: #fff;
|
| 5 |
+
--border: #e5e4e7;
|
| 6 |
+
--code-bg: #f4f3ec;
|
| 7 |
+
--accent: #aa3bff;
|
| 8 |
+
--accent-bg: rgba(170, 59, 255, 0.1);
|
| 9 |
+
--accent-border: rgba(170, 59, 255, 0.5);
|
| 10 |
+
--social-bg: rgba(244, 243, 236, 0.5);
|
| 11 |
+
--shadow:
|
| 12 |
+
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
| 13 |
+
|
| 14 |
+
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
| 15 |
+
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
| 16 |
+
--mono: ui-monospace, Consolas, monospace;
|
| 17 |
+
|
| 18 |
+
font: 18px/145% var(--sans);
|
| 19 |
+
letter-spacing: 0.18px;
|
| 20 |
+
color-scheme: light dark;
|
| 21 |
+
color: var(--text);
|
| 22 |
+
background: var(--bg);
|
| 23 |
+
font-synthesis: none;
|
| 24 |
+
text-rendering: optimizeLegibility;
|
| 25 |
+
-webkit-font-smoothing: antialiased;
|
| 26 |
+
-moz-osx-font-smoothing: grayscale;
|
| 27 |
+
|
| 28 |
+
@media (max-width: 1024px) {
|
| 29 |
+
font-size: 16px;
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
@media (prefers-color-scheme: dark) {
|
| 34 |
+
:root {
|
| 35 |
+
--text: #9ca3af;
|
| 36 |
+
--text-h: #f3f4f6;
|
| 37 |
+
--bg: #16171d;
|
| 38 |
+
--border: #2e303a;
|
| 39 |
+
--code-bg: #1f2028;
|
| 40 |
+
--accent: #c084fc;
|
| 41 |
+
--accent-bg: rgba(192, 132, 252, 0.15);
|
| 42 |
+
--accent-border: rgba(192, 132, 252, 0.5);
|
| 43 |
+
--social-bg: rgba(47, 48, 58, 0.5);
|
| 44 |
+
--shadow:
|
| 45 |
+
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
#social .button-icon {
|
| 49 |
+
filter: invert(1) brightness(2);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
body {
|
| 54 |
+
margin: 0;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#root {
|
| 58 |
+
width: 1126px;
|
| 59 |
+
max-width: 100%;
|
| 60 |
+
margin: 0 auto;
|
| 61 |
+
text-align: center;
|
| 62 |
+
border-inline: 1px solid var(--border);
|
| 63 |
+
min-height: 100svh;
|
| 64 |
+
display: flex;
|
| 65 |
+
flex-direction: column;
|
| 66 |
+
box-sizing: border-box;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
h1,
|
| 70 |
+
h2 {
|
| 71 |
+
font-family: var(--heading);
|
| 72 |
+
font-weight: 500;
|
| 73 |
+
color: var(--text-h);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
h1 {
|
| 77 |
+
font-size: 56px;
|
| 78 |
+
letter-spacing: -1.68px;
|
| 79 |
+
margin: 32px 0;
|
| 80 |
+
@media (max-width: 1024px) {
|
| 81 |
+
font-size: 36px;
|
| 82 |
+
margin: 20px 0;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
h2 {
|
| 86 |
+
font-size: 24px;
|
| 87 |
+
line-height: 118%;
|
| 88 |
+
letter-spacing: -0.24px;
|
| 89 |
+
margin: 0 0 8px;
|
| 90 |
+
@media (max-width: 1024px) {
|
| 91 |
+
font-size: 20px;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
p {
|
| 95 |
+
margin: 0;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
code,
|
| 99 |
+
.counter {
|
| 100 |
+
font-family: var(--mono);
|
| 101 |
+
display: inline-flex;
|
| 102 |
+
border-radius: 4px;
|
| 103 |
+
color: var(--text-h);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
code {
|
| 107 |
+
font-size: 15px;
|
| 108 |
+
line-height: 135%;
|
| 109 |
+
padding: 4px 8px;
|
| 110 |
+
background: var(--code-bg);
|
| 111 |
+
}
|
frontend/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App.jsx'
|
| 4 |
+
import './styles/tokens.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>
|
| 10 |
+
)
|
frontend/src/screens/AnalyzeScreen.jsx
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { Button, Card, Label, Icon, Spinner, LangBadge } from '../components/primitives'
|
| 3 |
+
import CodeEditor from '../components/CodeEditor'
|
| 4 |
+
import ResultGauge from '../components/ResultGauge'
|
| 5 |
+
import { FeatureCard } from '../components/FeatureCard'
|
| 6 |
+
import { analyzeCode } from '../api/client'
|
| 7 |
+
import ExplanationPanel from '../components/ExplanationPanel'
|
| 8 |
+
|
| 9 |
+
const SAMPLE = `def calculate_factorial_recursive(number_to_compute):
|
| 10 |
+
"""Calculate factorial using recursive approach with input validation."""
|
| 11 |
+
if not isinstance(number_to_compute, int):
|
| 12 |
+
raise TypeError("Input must be an integer value")
|
| 13 |
+
if number_to_compute < 0:
|
| 14 |
+
raise ValueError("Factorial undefined for negative numbers")
|
| 15 |
+
if number_to_compute <= 1:
|
| 16 |
+
return 1
|
| 17 |
+
return number_to_compute * calculate_factorial_recursive(number_to_compute - 1)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main_execution_function():
|
| 21 |
+
"""Main entry point for the factorial computation program."""
|
| 22 |
+
user_input_value = 10
|
| 23 |
+
final_result = calculate_factorial_recursive(user_input_value)
|
| 24 |
+
print(f"The factorial of {user_input_value} is {final_result}")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
main_execution_function()`
|
| 29 |
+
|
| 30 |
+
function severityFor(key, value) {
|
| 31 |
+
const highKeys = ['avg_identifier_length', 'avg_function_name_length', 'naming_consistency', 'comment_ratio', 'num_docstrings']
|
| 32 |
+
const medKeys = ['token_entropy', 'char_entropy', 'cyclomatic_complexity_approx', 'avg_function_length']
|
| 33 |
+
if (highKeys.includes(key)) return value > 5 ? 'high' : 'medium'
|
| 34 |
+
if (medKeys.includes(key)) return 'medium'
|
| 35 |
+
return 'low'
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
function formatFeatureVal(key, val) {
|
| 39 |
+
if (typeof val === 'number') {
|
| 40 |
+
if (key.includes('ratio') || key.includes('density')) return (val * 100).toFixed(1) + '%'
|
| 41 |
+
if (key.includes('entropy')) return val.toFixed(2)
|
| 42 |
+
if (Number.isInteger(val)) return String(val)
|
| 43 |
+
return val.toFixed(2)
|
| 44 |
+
}
|
| 45 |
+
return String(val)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const FEATURE_LABELS = {
|
| 49 |
+
avg_identifier_length: { label: 'Avg. identifier length', icon: 'code' },
|
| 50 |
+
naming_consistency: { label: 'Naming consistency', icon: 'sparkles' },
|
| 51 |
+
comment_ratio: { label: 'Comment density', icon: 'fileCode' },
|
| 52 |
+
token_entropy: { label: 'Token entropy', icon: 'chart' },
|
| 53 |
+
cyclomatic_complexity_approx: { label: 'Avg. complexity', icon: 'chart' },
|
| 54 |
+
avg_function_length: { label: 'Avg. function length', icon: 'code' },
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export default function AnalyzeScreen() {
|
| 58 |
+
const [code, setCode] = useState(SAMPLE)
|
| 59 |
+
const [result, setResult] = useState(null)
|
| 60 |
+
const [loading, setLoading] = useState(false)
|
| 61 |
+
const [error, setError] = useState(null)
|
| 62 |
+
|
| 63 |
+
const run = async () => {
|
| 64 |
+
if (!code.trim()) return
|
| 65 |
+
setLoading(true); setError(null); setResult(null)
|
| 66 |
+
try {
|
| 67 |
+
const data = await analyzeCode(code)
|
| 68 |
+
setResult(data)
|
| 69 |
+
} catch (e) {
|
| 70 |
+
setError(e.message)
|
| 71 |
+
} finally {
|
| 72 |
+
setLoading(false)
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
const topFeatures = result ? Object.entries(FEATURE_LABELS).map(([key, meta]) => ({
|
| 77 |
+
name: meta.label,
|
| 78 |
+
icon: meta.icon,
|
| 79 |
+
value: formatFeatureVal(key, result.all_features?.[key] ?? 0),
|
| 80 |
+
severity: severityFor(key, result.all_features?.[key] ?? 0),
|
| 81 |
+
})) : []
|
| 82 |
+
|
| 83 |
+
const lang = result?.detected_language
|
| 84 |
+
const langDisplay = lang ? lang.charAt(0).toUpperCase() + lang.slice(1) : 'Python'
|
| 85 |
+
|
| 86 |
+
return (
|
| 87 |
+
<div>
|
| 88 |
+
{/* Hero */}
|
| 89 |
+
<section style={{ padding: '80px 80px 48px', textAlign: 'center', position: 'relative' }}>
|
| 90 |
+
<div style={{
|
| 91 |
+
position: 'absolute', inset: 0,
|
| 92 |
+
backgroundImage: 'linear-gradient(to right, rgba(148,163,184,0.04) 1px, transparent 1px), linear-gradient(to bottom, rgba(148,163,184,0.04) 1px, transparent 1px)',
|
| 93 |
+
backgroundSize: '32px 32px', pointerEvents: 'none',
|
| 94 |
+
maskImage: 'radial-gradient(ellipse at center, black 30%, transparent 70%)',
|
| 95 |
+
}}/>
|
| 96 |
+
<div style={{ position: 'relative' }}>
|
| 97 |
+
<div style={{
|
| 98 |
+
display: 'inline-flex', alignItems: 'center', gap: 8,
|
| 99 |
+
padding: '6px 14px', borderRadius: 999,
|
| 100 |
+
background: 'rgba(0,212,255,0.08)', border: '1px solid rgba(0,212,255,0.20)',
|
| 101 |
+
marginBottom: 24,
|
| 102 |
+
}}>
|
| 103 |
+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#00D4FF', boxShadow: '0 0 8px #00D4FF' }}/>
|
| 104 |
+
<span style={{ fontSize: 12, color: '#00D4FF', fontWeight: 500, letterSpacing: '0.04em' }}>v1.0 · 42 statistical signals</span>
|
| 105 |
+
</div>
|
| 106 |
+
<h1 style={{ fontSize: 56, fontWeight: 700, letterSpacing: '-0.02em', lineHeight: 1.05, color: '#F8FAFC', margin: '0 0 20px', maxWidth: 800, marginLeft: 'auto', marginRight: 'auto' }}>
|
| 107 |
+
Detect <span style={{ color: '#00D4FF' }}>AI-generated</span> code instantly
|
| 108 |
+
</h1>
|
| 109 |
+
<p style={{ fontSize: 18, color: '#94A3B8', lineHeight: 1.55, maxWidth: 600, margin: '0 auto 32px' }}>
|
| 110 |
+
CodeSentinel analyzes 42 statistical signals — identifier patterns, comment density, structural rhythm — to estimate the probability that a submission was written by a large language model.
|
| 111 |
+
</p>
|
| 112 |
+
<Button variant="primary" size="lg" icon="play" onClick={() => document.getElementById('analyzer')?.scrollIntoView({ behavior: 'smooth', block: 'start' })}>
|
| 113 |
+
Start analyzing
|
| 114 |
+
</Button>
|
| 115 |
+
</div>
|
| 116 |
+
</section>
|
| 117 |
+
|
| 118 |
+
{/* Analyzer */}
|
| 119 |
+
<section id="analyzer" style={{ padding: '24px 80px 80px', maxWidth: 1280, margin: '0 auto' }}>
|
| 120 |
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: 24, marginBottom: 32 }}>
|
| 121 |
+
<div>
|
| 122 |
+
<Label style={{ marginBottom: 10 }}>Paste or edit code</Label>
|
| 123 |
+
<CodeEditor code={code} onChange={setCode} lang={langDisplay} height={380}/>
|
| 124 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 16 }}>
|
| 125 |
+
<div style={{ fontSize: 12, color: '#64748B', fontFamily: 'JetBrains Mono, monospace' }}>
|
| 126 |
+
{code.split('\n').length} lines · {code.length} chars
|
| 127 |
+
</div>
|
| 128 |
+
<Button variant="primary" icon="play" onClick={run} disabled={loading || !code.trim()}>
|
| 129 |
+
{loading ? 'Analyzing…' : 'Analyze code'}
|
| 130 |
+
</Button>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<Card glow style={{ padding: 24, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: 340 }}>
|
| 135 |
+
{loading ? (
|
| 136 |
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
|
| 137 |
+
<Spinner size={40}/>
|
| 138 |
+
<div style={{ fontSize: 13, color: '#94A3B8', letterSpacing: '0.04em', textTransform: 'uppercase' }}>Analyzing…</div>
|
| 139 |
+
</div>
|
| 140 |
+
) : error ? (
|
| 141 |
+
<div style={{ textAlign: 'center', padding: 20 }}>
|
| 142 |
+
<Icon name="alert" size={32} color="#EF4444"/>
|
| 143 |
+
<div style={{ fontSize: 13, color: '#EF4444', marginTop: 12 }}>{error}</div>
|
| 144 |
+
<div style={{ fontSize: 11, color: '#64748B', marginTop: 6 }}>Make sure the Flask server is running</div>
|
| 145 |
+
</div>
|
| 146 |
+
) : result ? (
|
| 147 |
+
<ResultGauge value={Math.round(result.ai_probability * 100)} size={200}/>
|
| 148 |
+
) : (
|
| 149 |
+
<div style={{ padding: 40, textAlign: 'center' }}>
|
| 150 |
+
<Icon name="sparkles" size={32} color="#475569"/>
|
| 151 |
+
<div style={{ fontSize: 14, color: '#64748B', marginTop: 14 }}>Run an analysis to see results</div>
|
| 152 |
+
</div>
|
| 153 |
+
)}
|
| 154 |
+
</Card>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
{result && (
|
| 158 |
+
<div style={{ animation: 'fadeIn .3s ease' }}>
|
| 159 |
+
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 18 }}>
|
| 160 |
+
<h2 style={{ fontSize: 24, fontWeight: 600, color: '#F8FAFC', margin: 0, letterSpacing: '-0.01em' }}>Feature breakdown</h2>
|
| 161 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
| 162 |
+
<LangBadge lang={langDisplay}/>
|
| 163 |
+
<span style={{ fontSize: 12, color: '#64748B' }}>Top 6 of 42 signals · sorted by suspicion</span>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginBottom: 32 }}>
|
| 167 |
+
{topFeatures.map((f, i) => <FeatureCard key={i} {...f}/>)}
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
{result.top_features?.length > 0 && (
|
| 171 |
+
<Card style={{ padding: 24 }}>
|
| 172 |
+
<Label style={{ marginBottom: 16 }}>Key signals</Label>
|
| 173 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
| 174 |
+
{result.top_features.map((f, i) => (
|
| 175 |
+
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
| 176 |
+
<span style={{ fontSize: 13, color: '#CBD5E1' }}>{f.name.replace(/_/g, ' ')}</span>
|
| 177 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
| 178 |
+
<div style={{ width: 80, height: 3, background: 'rgba(148,163,184,0.10)', borderRadius: 2, overflow: 'hidden' }}>
|
| 179 |
+
<div style={{ width: `${Math.round(f.importance * 1000)}%`, height: '100%', background: '#00D4FF' }}/>
|
| 180 |
+
</div>
|
| 181 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 12, color: '#94A3B8', minWidth: 40, textAlign: 'right' }}>
|
| 182 |
+
{f.value}
|
| 183 |
+
</span>
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
))}
|
| 187 |
+
</div>
|
| 188 |
+
</Card>
|
| 189 |
+
)}
|
| 190 |
+
|
| 191 |
+
{/* Explanations panel */}
|
| 192 |
+
<ExplanationPanel
|
| 193 |
+
explanations={result.explanations}
|
| 194 |
+
verdict={result.verdict}
|
| 195 |
+
aiProbability={result.ai_probability}
|
| 196 |
+
/>
|
| 197 |
+
</div>
|
| 198 |
+
)}
|
| 199 |
+
</section>
|
| 200 |
+
</div>
|
| 201 |
+
)
|
| 202 |
+
}
|
frontend/src/screens/BatchScreen.jsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { Button, Card, Label, Icon, Spinner, Input } from '../components/primitives'
|
| 3 |
+
import Dropzone from '../components/Dropzone'
|
| 4 |
+
import ResultsTable from '../components/ResultsTable'
|
| 5 |
+
import { StatCard } from '../components/FeatureCard'
|
| 6 |
+
import { analyzeBatch } from '../api/client'
|
| 7 |
+
|
| 8 |
+
export default function BatchScreen({ onOpenRow }) {
|
| 9 |
+
const [files, setFiles] = useState([])
|
| 10 |
+
const [results, setResults] = useState([])
|
| 11 |
+
const [summary, setSummary] = useState(null)
|
| 12 |
+
const [loading, setLoading] = useState(false)
|
| 13 |
+
const [progress, setProgress] = useState(0)
|
| 14 |
+
const [error, setError] = useState(null)
|
| 15 |
+
const [search, setSearch] = useState('')
|
| 16 |
+
|
| 17 |
+
const handleFiles = (fileList) => {
|
| 18 |
+
const arr = Array.from(fileList)
|
| 19 |
+
setFiles(arr); setResults([]); setSummary(null); setError(null)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const runBatch = async () => {
|
| 23 |
+
if (!files.length) return
|
| 24 |
+
setLoading(true); setError(null); setProgress(0)
|
| 25 |
+
|
| 26 |
+
const submissions = await Promise.all(files.map(async (f) => ({
|
| 27 |
+
id: f.name.replace(/\.[^.]+$/, ''),
|
| 28 |
+
file: f.name,
|
| 29 |
+
code: await f.text(),
|
| 30 |
+
})))
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
const data = await analyzeBatch(submissions)
|
| 34 |
+
const withFile = data.results.map((r, i) => ({ ...r, file: submissions[i]?.file || r.id }))
|
| 35 |
+
setResults(withFile)
|
| 36 |
+
setSummary(data.summary)
|
| 37 |
+
setProgress(100)
|
| 38 |
+
} catch (e) {
|
| 39 |
+
setError(e.message)
|
| 40 |
+
} finally {
|
| 41 |
+
setLoading(false)
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const exportCSV = () => {
|
| 46 |
+
if (!results.length) return
|
| 47 |
+
const headers = ['id', 'file', 'detected_language', 'ai_probability', 'verdict']
|
| 48 |
+
const rows = results.map(r => headers.map(h => {
|
| 49 |
+
const v = r[h]
|
| 50 |
+
return typeof v === 'number' ? (h === 'ai_probability' ? Math.round(v * 100) + '%' : v) : (v || '')
|
| 51 |
+
}))
|
| 52 |
+
const csv = [headers, ...rows].map(r => r.join(',')).join('\n')
|
| 53 |
+
const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(new Blob([csv])), download: 'codesentinel_results.csv' })
|
| 54 |
+
a.click()
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
const filtered = results.filter(r =>
|
| 58 |
+
!search || r.id?.toLowerCase().includes(search.toLowerCase()) || r.file?.toLowerCase().includes(search.toLowerCase())
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
return (
|
| 62 |
+
<div style={{ padding: '40px 80px 80px', maxWidth: 1280, margin: '0 auto' }}>
|
| 63 |
+
<div style={{ marginBottom: 32 }}>
|
| 64 |
+
<Label style={{ marginBottom: 8 }}>Batch analysis</Label>
|
| 65 |
+
<h1 style={{ fontSize: 40, fontWeight: 700, letterSpacing: '-0.02em', color: '#F8FAFC', margin: 0, lineHeight: 1.1 }}>
|
| 66 |
+
Class overview
|
| 67 |
+
</h1>
|
| 68 |
+
<p style={{ fontSize: 15, color: '#94A3B8', marginTop: 12, marginBottom: 0, maxWidth: 600 }}>
|
| 69 |
+
Drop a folder of submissions, then sort by AI probability to triage your grading queue.
|
| 70 |
+
</p>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
{/* Upload zone */}
|
| 74 |
+
{!results.length && (
|
| 75 |
+
<div style={{ marginBottom: 24 }}>
|
| 76 |
+
<Dropzone files={files} onUpload={handleFiles}/>
|
| 77 |
+
{files.length > 0 && (
|
| 78 |
+
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
| 79 |
+
<Button variant="primary" icon="play" onClick={runBatch} disabled={loading}>
|
| 80 |
+
{loading ? 'Analyzing…' : `Analyze ${files.length} file${files.length !== 1 ? 's' : ''}`}
|
| 81 |
+
</Button>
|
| 82 |
+
</div>
|
| 83 |
+
)}
|
| 84 |
+
</div>
|
| 85 |
+
)}
|
| 86 |
+
|
| 87 |
+
{loading && (
|
| 88 |
+
<Card style={{ padding: 40, textAlign: 'center', marginBottom: 24 }}>
|
| 89 |
+
<Spinner size={40} style={{ margin: '0 auto 16px' }}/>
|
| 90 |
+
<div style={{ fontSize: 14, color: '#94A3B8' }}>Analyzing {files.length} files…</div>
|
| 91 |
+
<div style={{ width: 240, height: 4, background: 'rgba(148,163,184,0.10)', borderRadius: 2, margin: '16px auto 0', overflow: 'hidden' }}>
|
| 92 |
+
<div style={{ width: '60%', height: '100%', background: '#00D4FF', animation: 'pulse 1.2s ease-in-out infinite' }}/>
|
| 93 |
+
</div>
|
| 94 |
+
</Card>
|
| 95 |
+
)}
|
| 96 |
+
|
| 97 |
+
{error && (
|
| 98 |
+
<Card style={{ padding: 20, marginBottom: 24, borderColor: 'rgba(239,68,68,0.35)', background: 'rgba(239,68,68,0.06)' }}>
|
| 99 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
| 100 |
+
<Icon name="alert" size={16} color="#EF4444"/>
|
| 101 |
+
<span style={{ fontSize: 14, color: '#EF4444' }}>{error}</span>
|
| 102 |
+
</div>
|
| 103 |
+
</Card>
|
| 104 |
+
)}
|
| 105 |
+
|
| 106 |
+
{summary && (
|
| 107 |
+
<>
|
| 108 |
+
{/* Stat cards */}
|
| 109 |
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
| 110 |
+
<StatCard label="Total submissions" value={summary.total} delta="files analyzed" icon="users" iconColor="#00D4FF"/>
|
| 111 |
+
<StatCard label="High risk" value={summary.high_risk} delta="≥ 70% AI probability" deltaColor="#EF4444" icon="alert" iconColor="#EF4444"/>
|
| 112 |
+
<StatCard label="Medium risk" value={summary.medium_risk} delta="40–69% probability" deltaColor="#F59E0B" icon="alert" iconColor="#F59E0B"/>
|
| 113 |
+
<StatCard label="Low risk" value={summary.low_risk} delta="< 40% probability" deltaColor="#10B981" icon="check" iconColor="#10B981"/>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
{/* Success bar */}
|
| 117 |
+
<div style={{ marginBottom: 24, padding: '14px 18px', background: 'rgba(16,185,129,0.06)', border: '1px solid rgba(16,185,129,0.20)', borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
| 118 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
| 119 |
+
<Icon name="check" size={16} color="#10B981"/>
|
| 120 |
+
<span style={{ fontSize: 13, color: '#CBD5E1' }}>Analyzed <strong style={{ color: '#F8FAFC' }}>{summary.total} files</strong> — avg. AI probability: <strong style={{ color: '#F8FAFC' }}>{Math.round(summary.avg_ai_probability * 100)}%</strong></span>
|
| 121 |
+
</div>
|
| 122 |
+
<div style={{ display: 'flex', gap: 8 }}>
|
| 123 |
+
<Button variant="secondary" size="sm" icon="upload" onClick={() => { setResults([]); setSummary(null); setFiles([]) }}>Re-upload</Button>
|
| 124 |
+
<Button variant="primary" size="sm" icon="download" onClick={exportCSV}>Export CSV</Button>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
{/* Toolbar */}
|
| 129 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
| 130 |
+
<Input icon="search" placeholder="Search by student ID or filename…" value={search} onChange={setSearch} style={{ width: 320 }}/>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<ResultsTable rows={filtered} onOpen={onOpenRow}/>
|
| 134 |
+
</>
|
| 135 |
+
)}
|
| 136 |
+
</div>
|
| 137 |
+
)
|
| 138 |
+
}
|
frontend/src/screens/DetailScreen.jsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Button, Card, Badge, Label, Icon, LangBadge } from '../components/primitives'
|
| 2 |
+
import CodeEditor from '../components/CodeEditor'
|
| 3 |
+
import ResultGauge from '../components/ResultGauge'
|
| 4 |
+
import DetailSidebar from '../components/DetailSidebar'
|
| 5 |
+
import ExplanationPanel from '../components/ExplanationPanel'
|
| 6 |
+
|
| 7 |
+
function buildFeatures(allFeatures) {
|
| 8 |
+
if (!allFeatures) return []
|
| 9 |
+
|
| 10 |
+
const styleKeys = ['avg_identifier_length', 'avg_function_name_length', 'single_char_name_ratio', 'naming_consistency', 'comment_ratio', 'num_docstrings', 'lexical_diversity', 'trailing_whitespace_ratio']
|
| 11 |
+
const structKeys = ['num_functions', 'avg_function_length', 'max_nesting_depth', 'cyclomatic_complexity_approx', 'if_density', 'for_density', 'ast_depth']
|
| 12 |
+
const statKeys = ['char_entropy', 'token_entropy', 'perplexity']
|
| 13 |
+
|
| 14 |
+
const toFeature = (key, category) => {
|
| 15 |
+
const val = allFeatures[key]
|
| 16 |
+
if (val === undefined || val === null) return null
|
| 17 |
+
const fmtVal = typeof val === 'number'
|
| 18 |
+
? (key.includes('ratio') || key.includes('density') ? (val * 100).toFixed(1) + '%' : val.toFixed ? val.toFixed(2) : String(val))
|
| 19 |
+
: String(val)
|
| 20 |
+
const severity = key.includes('length') && val > 6 ? 'high'
|
| 21 |
+
: key === 'naming_consistency' && val > 0.7 ? 'high'
|
| 22 |
+
: key === 'comment_ratio' && val > 0.2 ? 'high'
|
| 23 |
+
: key.includes('entropy') ? 'medium'
|
| 24 |
+
: 'low'
|
| 25 |
+
return { name: key.replace(/_/g, ' '), value: fmtVal, severity, category }
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
return [
|
| 29 |
+
...styleKeys.map(k => toFeature(k, 'Style')),
|
| 30 |
+
...structKeys.map(k => toFeature(k, 'Structure')),
|
| 31 |
+
...statKeys.map(k => toFeature(k, 'Statistical')),
|
| 32 |
+
].filter(Boolean)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function buildAnnotations(topFeatures) {
|
| 36 |
+
if (!topFeatures?.length) return []
|
| 37 |
+
return topFeatures.slice(0, 2).map((f, i) => ({
|
| 38 |
+
line: (i + 1) * 2,
|
| 39 |
+
tone: f.importance > 0.06 ? 'red' : 'amber',
|
| 40 |
+
note: `Suspicious: ${f.name.replace(/_/g, ' ')}`,
|
| 41 |
+
}))
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export default function DetailScreen({ row, onBack }) {
|
| 45 |
+
if (!row) return null
|
| 46 |
+
|
| 47 |
+
const prob = Math.round((row.ai_probability || 0) * 100)
|
| 48 |
+
const tone = prob >= 70 ? 'red' : prob >= 40 ? 'amber' : 'green'
|
| 49 |
+
const label = prob >= 70 ? 'LIKELY AI' : prob >= 40 ? 'POSSIBLY AI' : 'LIKELY HUMAN'
|
| 50 |
+
const lang = row.detected_language || 'python'
|
| 51 |
+
const langDisplay = lang.charAt(0).toUpperCase() + lang.slice(1)
|
| 52 |
+
|
| 53 |
+
const features = buildFeatures(row.all_features)
|
| 54 |
+
const annotations = buildAnnotations(row.top_features)
|
| 55 |
+
const code = row.code || '// Code not available'
|
| 56 |
+
|
| 57 |
+
return (
|
| 58 |
+
<div style={{ padding: '32px 80px 80px', maxWidth: 1280, margin: '0 auto' }}>
|
| 59 |
+
<button onClick={onBack} style={{
|
| 60 |
+
background: 'transparent', border: 'none', color: '#94A3B8', cursor: 'pointer',
|
| 61 |
+
display: 'inline-flex', alignItems: 'center', gap: 6, padding: 0, marginBottom: 20, fontSize: 13,
|
| 62 |
+
}}>
|
| 63 |
+
<Icon name="arrowLeft" size={14}/> Back to batch
|
| 64 |
+
</button>
|
| 65 |
+
|
| 66 |
+
{/* Header */}
|
| 67 |
+
<Card style={{ marginBottom: 24, padding: 24 }}>
|
| 68 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 32 }}>
|
| 69 |
+
<div>
|
| 70 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 }}>
|
| 71 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 24, fontWeight: 700, color: '#F8FAFC' }}>{row.id}</span>
|
| 72 |
+
<LangBadge lang={langDisplay}/>
|
| 73 |
+
<Badge tone={tone}>{label}</Badge>
|
| 74 |
+
</div>
|
| 75 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#94A3B8', fontSize: 13 }}>
|
| 76 |
+
<Icon name="fileCode" size={14}/>
|
| 77 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace' }}>{row.file || row.id}</span>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
<ResultGauge value={prob} size={140} animate={false}/>
|
| 81 |
+
</div>
|
| 82 |
+
</Card>
|
| 83 |
+
|
| 84 |
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: 24, alignItems: 'start' }}>
|
| 85 |
+
<div>
|
| 86 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
| 87 |
+
<Label style={{ marginBottom: 0 }}>Annotated source</Label>
|
| 88 |
+
{annotations.length > 0 && (
|
| 89 |
+
<span style={{ fontSize: 11, color: '#64748B' }}>{annotations.length} suspicious lines flagged</span>
|
| 90 |
+
)}
|
| 91 |
+
</div>
|
| 92 |
+
<CodeEditor code={code} readOnly lang={langDisplay} height={420} annotations={annotations} filename={row.file}/>
|
| 93 |
+
|
| 94 |
+
<ExplanationPanel
|
| 95 |
+
explanations={row.explanations}
|
| 96 |
+
verdict={row.verdict}
|
| 97 |
+
aiProbability={row.ai_probability}
|
| 98 |
+
/>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<DetailSidebar features={features}/>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
)
|
| 105 |
+
}
|
frontend/src/screens/SimilarityScreen.jsx
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { Button, Card, Label, Icon, Input, Spinner } from '../components/primitives'
|
| 3 |
+
import Heatmap from '../components/Heatmap'
|
| 4 |
+
import { SuspiciousPair } from '../components/DetailSidebar'
|
| 5 |
+
import Dropzone from '../components/Dropzone'
|
| 6 |
+
import { computeSimilarity } from '../api/client'
|
| 7 |
+
|
| 8 |
+
export default function SimilarityScreen() {
|
| 9 |
+
const [files, setFiles] = useState([])
|
| 10 |
+
const [result, setResult] = useState(null)
|
| 11 |
+
const [loading, setLoading] = useState(false)
|
| 12 |
+
const [error, setError] = useState(null)
|
| 13 |
+
const [threshold, setThreshold] = useState(0.70)
|
| 14 |
+
|
| 15 |
+
const run = async () => {
|
| 16 |
+
if (files.length < 2) { setError('Need at least 2 files to compare.'); return }
|
| 17 |
+
setLoading(true); setError(null)
|
| 18 |
+
const submissions = await Promise.all(files.map(async f => ({
|
| 19 |
+
id: f.name.replace(/\.[^.]+$/, ''),
|
| 20 |
+
code: await f.text(),
|
| 21 |
+
})))
|
| 22 |
+
try {
|
| 23 |
+
const data = await computeSimilarity(submissions)
|
| 24 |
+
setResult(data)
|
| 25 |
+
} catch (e) {
|
| 26 |
+
setError(e.message)
|
| 27 |
+
} finally {
|
| 28 |
+
setLoading(false)
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const suspiciousPairs = result?.suspicious_pairs?.filter(p => p.similarity >= threshold) || []
|
| 33 |
+
|
| 34 |
+
return (
|
| 35 |
+
<div style={{ padding: '40px 80px 80px', maxWidth: 1280, margin: '0 auto' }}>
|
| 36 |
+
<div style={{ marginBottom: 32 }}>
|
| 37 |
+
<Label style={{ marginBottom: 8 }}>Cross-submission analysis</Label>
|
| 38 |
+
<h1 style={{ fontSize: 40, fontWeight: 700, letterSpacing: '-0.02em', color: '#F8FAFC', margin: 0, lineHeight: 1.1 }}>
|
| 39 |
+
Submission similarity
|
| 40 |
+
</h1>
|
| 41 |
+
<p style={{ fontSize: 15, color: '#94A3B8', marginTop: 12, marginBottom: 0, maxWidth: 640 }}>
|
| 42 |
+
High similarity between submissions may indicate shared AI prompts. Pairs above the threshold are surfaced below the heatmap.
|
| 43 |
+
</p>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
{!result && (
|
| 47 |
+
<div style={{ marginBottom: 24 }}>
|
| 48 |
+
<Dropzone files={files} onUpload={f => { setFiles(Array.from(f)); setResult(null); setError(null) }}/>
|
| 49 |
+
{files.length >= 2 && (
|
| 50 |
+
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
| 51 |
+
<Button variant="primary" icon="play" onClick={run} disabled={loading}>
|
| 52 |
+
{loading ? 'Computing…' : `Compare ${files.length} submissions`}
|
| 53 |
+
</Button>
|
| 54 |
+
</div>
|
| 55 |
+
)}
|
| 56 |
+
</div>
|
| 57 |
+
)}
|
| 58 |
+
|
| 59 |
+
{loading && (
|
| 60 |
+
<Card style={{ padding: 40, textAlign: 'center' }}>
|
| 61 |
+
<Spinner size={40} style={{ margin: '0 auto 16px' }}/>
|
| 62 |
+
<div style={{ fontSize: 14, color: '#94A3B8' }}>Computing pairwise similarity…</div>
|
| 63 |
+
</Card>
|
| 64 |
+
)}
|
| 65 |
+
|
| 66 |
+
{error && (
|
| 67 |
+
<Card style={{ padding: 20, marginBottom: 24, borderColor: 'rgba(239,68,68,0.35)' }}>
|
| 68 |
+
<div style={{ display: 'flex', gap: 10 }}><Icon name="alert" size={16} color="#EF4444"/><span style={{ color: '#EF4444', fontSize: 14 }}>{error}</span></div>
|
| 69 |
+
</Card>
|
| 70 |
+
)}
|
| 71 |
+
|
| 72 |
+
{result && (
|
| 73 |
+
<>
|
| 74 |
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 280px', gap: 24, alignItems: 'start', marginBottom: 40 }}>
|
| 75 |
+
<Card style={{ padding: 24 }}>
|
| 76 |
+
<Heatmap ids={result.ids} data={result.matrix}/>
|
| 77 |
+
</Card>
|
| 78 |
+
|
| 79 |
+
<Card style={{ padding: 18 }}>
|
| 80 |
+
<Label style={{ marginBottom: 14 }}>Filters</Label>
|
| 81 |
+
<div style={{ marginBottom: 18 }}>
|
| 82 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
| 83 |
+
<span style={{ fontSize: 12, color: '#CBD5E1' }}>Similarity threshold</span>
|
| 84 |
+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 12, color: '#00D4FF', fontWeight: 600 }}>{Math.round(threshold * 100)}%</span>
|
| 85 |
+
</div>
|
| 86 |
+
<input type="range" min="0" max="100" value={Math.round(threshold * 100)}
|
| 87 |
+
onChange={e => setThreshold(e.target.value / 100)}
|
| 88 |
+
style={{ width: '100%', accentColor: '#00D4FF' }}/>
|
| 89 |
+
</div>
|
| 90 |
+
<div style={{ padding: '12px 14px', background: 'rgba(0,212,255,0.06)', borderRadius: 8, border: '1px solid rgba(0,212,255,0.15)' }}>
|
| 91 |
+
<div style={{ fontSize: 12, color: '#94A3B8', marginBottom: 4 }}>Suspicious pairs</div>
|
| 92 |
+
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 24, fontWeight: 700, color: suspiciousPairs.length > 0 ? '#EF4444' : '#10B981' }}>
|
| 93 |
+
{suspiciousPairs.length}
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
<div style={{ marginTop: 14 }}>
|
| 97 |
+
<Button variant="secondary" size="sm" icon="upload" onClick={() => { setResult(null); setFiles([]) }} style={{ width: '100%' }}>
|
| 98 |
+
Re-upload
|
| 99 |
+
</Button>
|
| 100 |
+
</div>
|
| 101 |
+
</Card>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<div>
|
| 105 |
+
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 18 }}>
|
| 106 |
+
<h2 style={{ fontSize: 24, fontWeight: 600, color: '#F8FAFC', margin: 0, letterSpacing: '-0.01em' }}>Suspicious pairs</h2>
|
| 107 |
+
<span style={{ fontSize: 12, color: '#64748B' }}>{suspiciousPairs.length} pairs above {Math.round(threshold * 100)}%</span>
|
| 108 |
+
</div>
|
| 109 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
| 110 |
+
{suspiciousPairs.map((p, i) => (
|
| 111 |
+
<SuspiciousPair key={i} a={p.id_a} b={p.id_b} similarity={Math.round(p.similarity * 100)}/>
|
| 112 |
+
))}
|
| 113 |
+
{suspiciousPairs.length === 0 && (
|
| 114 |
+
<Card style={{ padding: 40, textAlign: 'center' }}>
|
| 115 |
+
<Icon name="check" size={32} color="#10B981"/>
|
| 116 |
+
<div style={{ fontSize: 14, color: '#94A3B8', marginTop: 10 }}>No pairs exceed the {Math.round(threshold * 100)}% threshold</div>
|
| 117 |
+
</Card>
|
| 118 |
+
)}
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</>
|
| 122 |
+
)}
|
| 123 |
+
</div>
|
| 124 |
+
)
|
| 125 |
+
}
|
frontend/src/styles/tokens.css
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--cs-canvas: #0A0F1C;
|
| 5 |
+
--cs-panel: #0F1729;
|
| 6 |
+
--cs-raised: #111827;
|
| 7 |
+
--cs-input: #1F2937;
|
| 8 |
+
--cs-input-soft: #131B2E;
|
| 9 |
+
--cs-cyan: #00D4FF;
|
| 10 |
+
--cs-cyan-strong: #22E0FF;
|
| 11 |
+
--cs-teal: #06B6D4;
|
| 12 |
+
--cs-cyan-soft: rgba(0,212,255,0.12);
|
| 13 |
+
--cs-cyan-glow: rgba(0,212,255,0.25);
|
| 14 |
+
--cs-green: #10B981;
|
| 15 |
+
--cs-green-soft: rgba(16,185,129,0.14);
|
| 16 |
+
--cs-amber: #F59E0B;
|
| 17 |
+
--cs-amber-soft: rgba(245,158,11,0.14);
|
| 18 |
+
--cs-red: #EF4444;
|
| 19 |
+
--cs-red-soft: rgba(239,68,68,0.14);
|
| 20 |
+
--cs-fg-1: #F8FAFC;
|
| 21 |
+
--cs-fg-2: #CBD5E1;
|
| 22 |
+
--cs-fg-3: #94A3B8;
|
| 23 |
+
--cs-fg-4: #64748B;
|
| 24 |
+
--cs-border: rgba(148,163,184,0.12);
|
| 25 |
+
--cs-border-hover: rgba(148,163,184,0.24);
|
| 26 |
+
--cs-border-focus: rgba(0,212,255,0.40);
|
| 27 |
+
--cs-font-sans: 'Inter', system-ui, sans-serif;
|
| 28 |
+
--cs-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
| 29 |
+
--cs-shadow-glow: 0 0 24px rgba(0,212,255,0.25);
|
| 30 |
+
--cs-shadow-lift: 0 8px 24px rgba(0,0,0,0.40);
|
| 31 |
+
--cs-shadow-inset: inset 0 1px 0 rgba(255,255,255,0.03);
|
| 32 |
+
--cs-ease: cubic-bezier(0.22,1,0.36,1);
|
| 33 |
+
--cs-dur: 240ms;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 37 |
+
|
| 38 |
+
body {
|
| 39 |
+
margin: 0;
|
| 40 |
+
background: var(--cs-canvas);
|
| 41 |
+
color: var(--cs-fg-2);
|
| 42 |
+
font-family: var(--cs-font-sans);
|
| 43 |
+
font-size: 14px;
|
| 44 |
+
-webkit-font-smoothing: antialiased;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
| 48 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 49 |
+
::-webkit-scrollbar-thumb { background: rgba(148,163,184,0.2); border-radius: 4px; }
|
| 50 |
+
::-webkit-scrollbar-thumb:hover { background: rgba(0,212,255,0.4); }
|
| 51 |
+
|
| 52 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 53 |
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
frontend/vite.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
port: 3000,
|
| 8 |
+
proxy: {
|
| 9 |
+
'/api': {
|
| 10 |
+
target: 'http://localhost:5000',
|
| 11 |
+
changeOrigin: true,
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
})
|
language_config.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import tree_sitter_python
|
| 3 |
+
import tree_sitter_javascript
|
| 4 |
+
import tree_sitter_java
|
| 5 |
+
import tree_sitter_cpp
|
| 6 |
+
import tree_sitter_c
|
| 7 |
+
import tree_sitter_go
|
| 8 |
+
import tree_sitter_rust
|
| 9 |
+
import tree_sitter_ruby
|
| 10 |
+
import tree_sitter_typescript
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
LANGUAGE_CONFIGS: dict = {
|
| 14 |
+
|
| 15 |
+
# ── Python ────────────────────────────────────────────────────────────
|
| 16 |
+
"python": {
|
| 17 |
+
"ts_module": tree_sitter_python,
|
| 18 |
+
"extensions": [".py"],
|
| 19 |
+
"inline_comment": r"^\s*#",
|
| 20 |
+
"block_comment": None,
|
| 21 |
+
"docstring_pattern": r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'',
|
| 22 |
+
"node_types": {
|
| 23 |
+
"function": ["function_definition"],
|
| 24 |
+
"class": ["class_definition"],
|
| 25 |
+
"if": ["if_statement"],
|
| 26 |
+
"for": ["for_statement"],
|
| 27 |
+
"while": ["while_statement"],
|
| 28 |
+
"try": ["try_statement"],
|
| 29 |
+
"import": ["import_statement", "import_from_statement"],
|
| 30 |
+
"identifier": ["identifier"],
|
| 31 |
+
"lambda": ["lambda"],
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
|
| 35 |
+
# ── JavaScript ────────────────────────────────────────────────────────
|
| 36 |
+
"javascript": {
|
| 37 |
+
"ts_module": tree_sitter_javascript,
|
| 38 |
+
"extensions": [".js", ".mjs", ".cjs"],
|
| 39 |
+
"inline_comment": r"^\s*//",
|
| 40 |
+
"block_comment": ("/*", "*/"),
|
| 41 |
+
"docstring_pattern": r"/\*\*[\s\S]*?\*/",
|
| 42 |
+
"node_types": {
|
| 43 |
+
"function": ["function_declaration", "arrow_function",
|
| 44 |
+
"function_expression", "method_definition"],
|
| 45 |
+
"class": ["class_declaration", "class"],
|
| 46 |
+
"if": ["if_statement"],
|
| 47 |
+
"for": ["for_statement", "for_in_statement"],
|
| 48 |
+
"while": ["while_statement"],
|
| 49 |
+
"try": ["try_statement"],
|
| 50 |
+
"import": ["import_statement"],
|
| 51 |
+
"identifier": ["identifier"],
|
| 52 |
+
"lambda": ["arrow_function"],
|
| 53 |
+
},
|
| 54 |
+
},
|
| 55 |
+
|
| 56 |
+
# ── TypeScript ────────────────────────────────────────────────────────
|
| 57 |
+
"typescript": {
|
| 58 |
+
"ts_module": tree_sitter_typescript,
|
| 59 |
+
"extensions": [".ts"],
|
| 60 |
+
"inline_comment": r"^\s*//",
|
| 61 |
+
"block_comment": ("/*", "*/"),
|
| 62 |
+
"docstring_pattern": r"/\*\*[\s\S]*?\*/",
|
| 63 |
+
"node_types": {
|
| 64 |
+
"function": ["function_declaration", "arrow_function",
|
| 65 |
+
"function_expression", "method_definition"],
|
| 66 |
+
"class": ["class_declaration"],
|
| 67 |
+
"if": ["if_statement"],
|
| 68 |
+
"for": ["for_statement", "for_in_statement"],
|
| 69 |
+
"while": ["while_statement"],
|
| 70 |
+
"try": ["try_statement"],
|
| 71 |
+
"import": ["import_statement"],
|
| 72 |
+
"identifier": ["identifier"],
|
| 73 |
+
"lambda": ["arrow_function"],
|
| 74 |
+
},
|
| 75 |
+
},
|
| 76 |
+
|
| 77 |
+
# ── Java ──────────────────────────────────────────────────────────────
|
| 78 |
+
"java": {
|
| 79 |
+
"ts_module": tree_sitter_java,
|
| 80 |
+
"extensions": [".java"],
|
| 81 |
+
"inline_comment": r"^\s*//",
|
| 82 |
+
"block_comment": ("/*", "*/"),
|
| 83 |
+
"docstring_pattern": r"/\*\*[\s\S]*?\*/",
|
| 84 |
+
"node_types": {
|
| 85 |
+
"function": ["method_declaration", "constructor_declaration"],
|
| 86 |
+
"class": ["class_declaration", "interface_declaration",
|
| 87 |
+
"enum_declaration"],
|
| 88 |
+
"if": ["if_statement"],
|
| 89 |
+
"for": ["for_statement", "enhanced_for_statement"],
|
| 90 |
+
"while": ["while_statement"],
|
| 91 |
+
"try": ["try_statement"],
|
| 92 |
+
"import": ["import_declaration"],
|
| 93 |
+
"identifier": ["identifier"],
|
| 94 |
+
"lambda": ["lambda_expression"],
|
| 95 |
+
},
|
| 96 |
+
},
|
| 97 |
+
|
| 98 |
+
# ── C ─────────────────────────────────────────────────────────────────
|
| 99 |
+
"c": {
|
| 100 |
+
"ts_module": tree_sitter_c,
|
| 101 |
+
"extensions": [".c", ".h"],
|
| 102 |
+
"inline_comment": r"^\s*//",
|
| 103 |
+
"block_comment": ("/*", "*/"),
|
| 104 |
+
"docstring_pattern": r"/\*\*[\s\S]*?\*/",
|
| 105 |
+
"node_types": {
|
| 106 |
+
"function": ["function_definition"],
|
| 107 |
+
"class": ["struct_specifier", "union_specifier"],
|
| 108 |
+
"if": ["if_statement"],
|
| 109 |
+
"for": ["for_statement"],
|
| 110 |
+
"while": ["while_statement"],
|
| 111 |
+
"try": [],
|
| 112 |
+
"import": ["preproc_include"],
|
| 113 |
+
"identifier": ["identifier"],
|
| 114 |
+
"lambda": [],
|
| 115 |
+
},
|
| 116 |
+
},
|
| 117 |
+
|
| 118 |
+
# ── C++ ───────────────────────────────────────────────────────────────
|
| 119 |
+
"cpp": {
|
| 120 |
+
"ts_module": tree_sitter_cpp,
|
| 121 |
+
"extensions": [".cpp", ".cc", ".cxx", ".hpp"],
|
| 122 |
+
"inline_comment": r"^\s*//",
|
| 123 |
+
"block_comment": ("/*", "*/"),
|
| 124 |
+
"docstring_pattern": r"/\*\*[\s\S]*?\*/",
|
| 125 |
+
"node_types": {
|
| 126 |
+
"function": ["function_definition"],
|
| 127 |
+
"class": ["class_specifier", "struct_specifier"],
|
| 128 |
+
"if": ["if_statement"],
|
| 129 |
+
"for": ["for_statement", "for_range_loop"],
|
| 130 |
+
"while": ["while_statement"],
|
| 131 |
+
"try": ["try_statement"],
|
| 132 |
+
"import": ["preproc_include"],
|
| 133 |
+
"identifier": ["identifier"],
|
| 134 |
+
"lambda": ["lambda_expression"],
|
| 135 |
+
},
|
| 136 |
+
},
|
| 137 |
+
|
| 138 |
+
# ── Go ────────────────────────────────────────────────────────────────
|
| 139 |
+
"go": {
|
| 140 |
+
"ts_module": tree_sitter_go,
|
| 141 |
+
"extensions": [".go"],
|
| 142 |
+
"inline_comment": r"^\s*//",
|
| 143 |
+
"block_comment": ("/*", "*/"),
|
| 144 |
+
"docstring_pattern": None,
|
| 145 |
+
"node_types": {
|
| 146 |
+
"function": ["function_declaration", "method_declaration"],
|
| 147 |
+
"class": ["type_declaration"],
|
| 148 |
+
"if": ["if_statement"],
|
| 149 |
+
"for": ["for_statement"],
|
| 150 |
+
"while": ["for_statement"],
|
| 151 |
+
"try": [],
|
| 152 |
+
"import": ["import_declaration"],
|
| 153 |
+
"identifier": ["identifier"],
|
| 154 |
+
"lambda": ["func_literal"],
|
| 155 |
+
},
|
| 156 |
+
},
|
| 157 |
+
|
| 158 |
+
# ── Rust ──────────────────────────────────────────────────────────────
|
| 159 |
+
"rust": {
|
| 160 |
+
"ts_module": tree_sitter_rust,
|
| 161 |
+
"extensions": [".rs"],
|
| 162 |
+
"inline_comment": r"^\s*//",
|
| 163 |
+
"block_comment": ("/*", "*/"),
|
| 164 |
+
"docstring_pattern": r"///.*",
|
| 165 |
+
"node_types": {
|
| 166 |
+
"function": ["function_item"],
|
| 167 |
+
"class": ["struct_item", "impl_item", "trait_item"],
|
| 168 |
+
"if": ["if_expression"],
|
| 169 |
+
"for": ["for_expression"],
|
| 170 |
+
"while": ["while_expression"],
|
| 171 |
+
"try": ["match_expression"],
|
| 172 |
+
"import": ["use_declaration"],
|
| 173 |
+
"identifier": ["identifier"],
|
| 174 |
+
"lambda": ["closure_expression"],
|
| 175 |
+
},
|
| 176 |
+
},
|
| 177 |
+
|
| 178 |
+
# ── Ruby ──────────────────────────────────────────────────────────────
|
| 179 |
+
"ruby": {
|
| 180 |
+
"ts_module": tree_sitter_ruby,
|
| 181 |
+
"extensions": [".rb"],
|
| 182 |
+
"inline_comment": r"^\s*#",
|
| 183 |
+
"block_comment": ("=begin", "=end"),
|
| 184 |
+
"docstring_pattern": None,
|
| 185 |
+
"node_types": {
|
| 186 |
+
"function": ["method", "singleton_method"],
|
| 187 |
+
"class": ["class", "module"],
|
| 188 |
+
"if": ["if", "unless"],
|
| 189 |
+
"for": ["for"],
|
| 190 |
+
"while": ["while", "until"],
|
| 191 |
+
"try": ["begin"],
|
| 192 |
+
"import": [],
|
| 193 |
+
"identifier": ["identifier"],
|
| 194 |
+
"lambda": ["lambda"],
|
| 195 |
+
},
|
| 196 |
+
},
|
| 197 |
+
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def get_config(language: str) -> dict:
|
| 202 |
+
key = language.lower().strip()
|
| 203 |
+
if key not in LANGUAGE_CONFIGS:
|
| 204 |
+
supported = ", ".join(sorted(LANGUAGE_CONFIGS.keys()))
|
| 205 |
+
raise ValueError(
|
| 206 |
+
f"Jezik '{language}' nije podržan.\n"
|
| 207 |
+
f"Podržani jezici: {supported}"
|
| 208 |
+
)
|
| 209 |
+
return LANGUAGE_CONFIGS[key]
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def detect_language_from_extension(filename: str):
|
| 213 |
+
|
| 214 |
+
filename = filename.lower()
|
| 215 |
+
for lang, config in LANGUAGE_CONFIGS.items():
|
| 216 |
+
for ext in config["extensions"]:
|
| 217 |
+
if filename.endswith(ext):
|
| 218 |
+
return lang
|
| 219 |
+
return None
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def detect_language_from_code(code: str) -> str:
|
| 223 |
+
|
| 224 |
+
scores = {lang: 0 for lang in LANGUAGE_CONFIGS}
|
| 225 |
+
|
| 226 |
+
# Python
|
| 227 |
+
if re.search(r"\bdef\s+\w+\s*\(", code): scores["python"] += 3
|
| 228 |
+
if re.search(r'"""', code): scores["python"] += 2
|
| 229 |
+
if re.search(r"\bself\b", code): scores["python"] += 2
|
| 230 |
+
if re.search(r"\bprint\s*\(", code): scores["python"] += 1
|
| 231 |
+
if re.search(r":\s*$", code, re.MULTILINE): scores["python"] += 1
|
| 232 |
+
|
| 233 |
+
# Java
|
| 234 |
+
if re.search(r"\bpublic\s+class\b", code): scores["java"] += 4
|
| 235 |
+
if re.search(r"\bSystem\.out\b", code): scores["java"] += 3
|
| 236 |
+
if re.search(r"\bpublic\s+static\b", code): scores["java"] += 2
|
| 237 |
+
if re.search(r"\bvoid\s+main\b", code): scores["java"] += 2
|
| 238 |
+
|
| 239 |
+
# JavaScript
|
| 240 |
+
if re.search(r"\bconst\s+\w+\s*=", code): scores["javascript"] += 2
|
| 241 |
+
if re.search(r"=>\s*[{(]", code): scores["javascript"] += 3
|
| 242 |
+
if re.search(r"\bconsole\.log\b", code): scores["javascript"] += 3
|
| 243 |
+
|
| 244 |
+
# TypeScript
|
| 245 |
+
if re.search(r":\s*(string|number|boolean|void)\b", code):
|
| 246 |
+
scores["typescript"] += 4
|
| 247 |
+
if re.search(r"\binterface\s+\w+", code): scores["typescript"] += 4
|
| 248 |
+
|
| 249 |
+
# C / C++
|
| 250 |
+
if re.search(r"#include\s*[<\"]", code):
|
| 251 |
+
scores["cpp"] += 3
|
| 252 |
+
scores["c"] += 3
|
| 253 |
+
if re.search(r"\bstd::", code): scores["cpp"] += 4
|
| 254 |
+
if re.search(r"\bcout\b", code): scores["cpp"] += 4
|
| 255 |
+
if re.search(r"\bcin\b", code): scores["cpp"] += 4
|
| 256 |
+
if re.search(r"\busing\s+namespace\b", code): scores["cpp"] += 4
|
| 257 |
+
if re.search(r"\btemplate\s*<", code): scores["cpp"] += 3
|
| 258 |
+
if re.search(r"\bvector\s*<", code): scores["cpp"] += 3
|
| 259 |
+
if re.search(r"\bstring\b", code) and re.search(r"#include", code):
|
| 260 |
+
scores["cpp"] += 2
|
| 261 |
+
if re.search(r"\bprintf\s*\(", code): scores["c"] += 3
|
| 262 |
+
if re.search(r"\bscanf\s*\(", code): scores["c"] += 3
|
| 263 |
+
if re.search(r"\bmalloc\s*\(", code): scores["c"] += 3
|
| 264 |
+
|
| 265 |
+
# Go
|
| 266 |
+
if re.search(r"\bfunc\s+\w+\s*\(", code): scores["go"] += 4
|
| 267 |
+
if re.search(r"\bpackage\s+\w+", code): scores["go"] += 4
|
| 268 |
+
if re.search(r"\bfmt\.Print", code): scores["go"] += 3
|
| 269 |
+
|
| 270 |
+
# Rust
|
| 271 |
+
if re.search(r"\bfn\s+\w+\s*\(", code): scores["rust"] += 4
|
| 272 |
+
if re.search(r"\blet\s+mut\b", code): scores["rust"] += 4
|
| 273 |
+
if re.search(r"\bprintln!\s*\(", code): scores["rust"] += 3
|
| 274 |
+
|
| 275 |
+
# Ruby
|
| 276 |
+
if re.search(r"\bdef\s+\w+", code) and re.search(r"\bend\b", code):
|
| 277 |
+
scores["ruby"] += 3
|
| 278 |
+
if re.search(r"\bputs\s+", code): scores["ruby"] += 3
|
| 279 |
+
|
| 280 |
+
best = max(scores, key=lambda l: scores[l])
|
| 281 |
+
return best if scores[best] > 0 else "python"
|
main.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from feature_extraction import extract_all_features
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 5 |
+
# UČITAVANJE MODELA
|
| 6 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 7 |
+
|
| 8 |
+
MODEL_NAME = "Salesforce/codegen-350M-mono"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def ucitaj_model():
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
print(f" Učitavam model '{MODEL_NAME}'...")
|
| 18 |
+
print(" (Pri prvom pokretanju ovo može potrajati par minuta.)\n")
|
| 19 |
+
|
| 20 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 21 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 22 |
+
MODEL_NAME,
|
| 23 |
+
torch_dtype=torch.float32, # float32 radi na CPU bez GPU-a
|
| 24 |
+
)
|
| 25 |
+
model.eval() # isključi dropout — samo inferenca, ne treniranje
|
| 26 |
+
|
| 27 |
+
num_params = sum(p.numel() for p in model.parameters()) // 1_000_000
|
| 28 |
+
print(f" Model učitan ({num_params}M parametara).\n")
|
| 29 |
+
return model, tokenizer
|
| 30 |
+
|
| 31 |
+
except ImportError:
|
| 32 |
+
print(" [UPOZORENJE] transformers ili torch nisu instalirani.")
|
| 33 |
+
print(" Pokreni: pip install transformers torch\n")
|
| 34 |
+
return None, None
|
| 35 |
+
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f" [UPOZORENJE] Model nije mogao biti učitan: {e}\n")
|
| 38 |
+
return None, None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 42 |
+
# UNOS KODA
|
| 43 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 44 |
+
|
| 45 |
+
def ucitaj_kod() -> str:
|
| 46 |
+
|
| 47 |
+
print("Zalijepi kod ispod, a kad završiš pritisni Enter dva puta:\n")
|
| 48 |
+
linije = []
|
| 49 |
+
prazni_zaredom = 0
|
| 50 |
+
|
| 51 |
+
while True:
|
| 52 |
+
linija = input()
|
| 53 |
+
if linija == "":
|
| 54 |
+
prazni_zaredom += 1
|
| 55 |
+
if prazni_zaredom >= 2:
|
| 56 |
+
break
|
| 57 |
+
linije.append(linija)
|
| 58 |
+
else:
|
| 59 |
+
prazni_zaredom = 0
|
| 60 |
+
linije.append(linija)
|
| 61 |
+
|
| 62 |
+
return "\n".join(linije).strip()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def ucitaj_iz_datoteke(putanja: str) -> str:
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
with open(putanja, "r", encoding="utf-8") as f:
|
| 69 |
+
sadrzaj = f.read()
|
| 70 |
+
print(f" Učitano {len(sadrzaj.splitlines())} linija iz '{putanja}'.")
|
| 71 |
+
return sadrzaj
|
| 72 |
+
except FileNotFoundError:
|
| 73 |
+
print(f"\n [GREŠKA] Datoteka '{putanja}' nije pronađena.")
|
| 74 |
+
print( " Provjeri je li datoteka u istom folderu kao main.py.")
|
| 75 |
+
return ""
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"\n [GREŠKA] Problem pri čitanju datoteke: {e}")
|
| 78 |
+
return ""
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 82 |
+
# ISPIS REZULTATA
|
| 83 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 84 |
+
|
| 85 |
+
def ispisi_znacajke(features: dict) -> None:
|
| 86 |
+
|
| 87 |
+
jezik = features.get("detected_language", "nepoznat")
|
| 88 |
+
print(f"\n{'═' * 55}")
|
| 89 |
+
print(f" Prepoznat jezik: {jezik.upper()}")
|
| 90 |
+
print(f"{'═' * 55}")
|
| 91 |
+
|
| 92 |
+
# ── Stilska detekcija ──────────────────────────────────────
|
| 93 |
+
print("\n [ STILSKA DETEKCIJA — komentari, imenovanje, formatiranje ]\n")
|
| 94 |
+
|
| 95 |
+
stilske = [
|
| 96 |
+
("Broj linija s komentarom", "num_comment_lines"),
|
| 97 |
+
("Udio komentara (%)", "comment_ratio"),
|
| 98 |
+
("Prosj. duljina komentara (rij.)", "avg_comment_length_words"),
|
| 99 |
+
("Udio komentara u znakovima", "comment_to_code_ratio"),
|
| 100 |
+
("Broj blok komentara", "num_block_comments"),
|
| 101 |
+
("Broj docstringova / JSDoc", "num_docstrings"),
|
| 102 |
+
("Prosj. duljina identifikatora", "avg_identifier_length"),
|
| 103 |
+
("Prosj. duljina naziva funkcija", "avg_function_name_length"),
|
| 104 |
+
("Udio jednoslovnih naziva", "single_char_name_ratio"),
|
| 105 |
+
("Leksička raznolikost naziva", "lexical_diversity"),
|
| 106 |
+
("Udio snake_case stila", "snake_case_ratio"),
|
| 107 |
+
("Udio camelCase stila", "camel_case_ratio"),
|
| 108 |
+
("Konzistentnost imenovanja", "naming_consistency"),
|
| 109 |
+
("Ukupno linija", "total_lines"),
|
| 110 |
+
("Udio praznih linija", "empty_line_ratio"),
|
| 111 |
+
("Prosj. duljina linije (znakovi)", "avg_line_length"),
|
| 112 |
+
("Max duljina linije", "max_line_length"),
|
| 113 |
+
("Koristi tabove (1=da, 0=ne)", "uses_tabs"),
|
| 114 |
+
("Udio linija s trailing space", "trailing_whitespace_ratio"),
|
| 115 |
+
("Konzistentnost razmaka operatori", "operator_spacing_consistency"),
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
for naziv, kljuc in stilske:
|
| 119 |
+
val = features.get(kljuc, "N/A")
|
| 120 |
+
if isinstance(val, float):
|
| 121 |
+
print(f" {naziv:<38} {val:.4f}")
|
| 122 |
+
else:
|
| 123 |
+
print(f" {naziv:<38} {val}")
|
| 124 |
+
|
| 125 |
+
# ── Strukturna detekcija ───────────────────────────────────
|
| 126 |
+
print("\n [ STRUKTURNA DETEKCIJA — AST, složenost, tok kontrole ]\n")
|
| 127 |
+
|
| 128 |
+
strukturne = [
|
| 129 |
+
("Dubina AST stabla", "ast_depth"),
|
| 130 |
+
("Broj čvorova u AST stablu", "ast_node_count"),
|
| 131 |
+
("Raznolikost tipova čvorova", "unique_node_type_ratio"),
|
| 132 |
+
("Broj funkcija / metoda", "num_functions"),
|
| 133 |
+
("Prosj. duljina funkcije (linije)", "avg_function_length"),
|
| 134 |
+
("Max duljina funkcije (linije)", "max_function_length"),
|
| 135 |
+
("Prosj. broj argumenata", "avg_args_per_function"),
|
| 136 |
+
("Broj klasa", "num_classes"),
|
| 137 |
+
("Broj importa", "num_imports"),
|
| 138 |
+
("Broj if naredbi", "num_if_statements"),
|
| 139 |
+
("Broj for petlji", "num_for_loops"),
|
| 140 |
+
("Broj while petlji", "num_while_loops"),
|
| 141 |
+
("Broj try/catch blokova", "num_try_blocks"),
|
| 142 |
+
("Broj lambda izraza", "num_lambdas"),
|
| 143 |
+
("Max dubina ugniježđenosti", "max_nesting_depth"),
|
| 144 |
+
("Prosj. dubina ugniježđenosti", "avg_nesting_depth"),
|
| 145 |
+
("Aproks. ciklomatska složenost", "cyclomatic_complexity_approx"),
|
| 146 |
+
]
|
| 147 |
+
|
| 148 |
+
for naziv, kljuc in strukturne:
|
| 149 |
+
val = features.get(kljuc, "N/A")
|
| 150 |
+
if isinstance(val, float):
|
| 151 |
+
print(f" {naziv:<38} {val:.4f}")
|
| 152 |
+
else:
|
| 153 |
+
print(f" {naziv:<38} {val}")
|
| 154 |
+
|
| 155 |
+
# ── Statistička detekcija ──────────────────────────────────
|
| 156 |
+
print("\n [ STATISTIČKA DETEKCIJA — perplexity ]\n")
|
| 157 |
+
|
| 158 |
+
perp = features.get("perplexity", -1.0)
|
| 159 |
+
dostupan = features.get("model_available", 0)
|
| 160 |
+
|
| 161 |
+
if dostupan == 0:
|
| 162 |
+
print(" Model nije učitan — perplexity nije izračunat.")
|
| 163 |
+
else:
|
| 164 |
+
print(f" {'Perplexity':<38} {perp:.4f}")
|
| 165 |
+
|
| 166 |
+
# Grubo tumačenje perplexityja za CodeGEN-350M
|
| 167 |
+
if perp < 5:
|
| 168 |
+
tumacenje = "vrlo nizak → vjerojatno AI"
|
| 169 |
+
elif perp < 20:
|
| 170 |
+
tumacenje = "nizak → moguće AI"
|
| 171 |
+
elif perp < 60:
|
| 172 |
+
tumacenje = "srednji → nejasno"
|
| 173 |
+
else:
|
| 174 |
+
tumacenje = "visok → vjerojatno čovjek"
|
| 175 |
+
print(f" {'Tumačenje':<38} {tumacenje}")
|
| 176 |
+
|
| 177 |
+
print(f"\n{'═' * 55}")
|
| 178 |
+
print(f" Ukupno izvučeno značajki: {len(features) - 1}")
|
| 179 |
+
print(f"{'═' * 55}\n")
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 183 |
+
# GLAVNI PROGRAM
|
| 184 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 185 |
+
|
| 186 |
+
def main():
|
| 187 |
+
print("=" * 55)
|
| 188 |
+
print(" Ekstraktor značajki koda")
|
| 189 |
+
print(" (za detekciju AI generiranog koda)")
|
| 190 |
+
print("=" * 55)
|
| 191 |
+
|
| 192 |
+
# Pitaj korisnika želi li učitati model za perplexity
|
| 193 |
+
print("\nŽeliš li učitati model za izračun perplexityja?")
|
| 194 |
+
print(" d — Da (skida ~700 MB pri prvom pokretanju)")
|
| 195 |
+
print(" n — Ne (perplexity će biti preskočen)\n")
|
| 196 |
+
odabir_model = input("Odabir (d/n): ").strip().lower()
|
| 197 |
+
|
| 198 |
+
model, tokenizer = None, None
|
| 199 |
+
if odabir_model in ("d", "da", "y", "yes"):
|
| 200 |
+
model, tokenizer = ucitaj_model()
|
| 201 |
+
|
| 202 |
+
# Način unosa koda
|
| 203 |
+
print("\nNačin unosa koda:")
|
| 204 |
+
print(" 1 — Zalijepi kod direktno u terminal")
|
| 205 |
+
print(" 2 — Učitaj iz datoteke (npr. kod.txt)")
|
| 206 |
+
|
| 207 |
+
while True:
|
| 208 |
+
print()
|
| 209 |
+
odabir = input("Odaberi način unosa (1 ili 2): ").strip()
|
| 210 |
+
|
| 211 |
+
if odabir == "1":
|
| 212 |
+
kod = ucitaj_kod()
|
| 213 |
+
|
| 214 |
+
elif odabir == "2":
|
| 215 |
+
putanja = input("Putanja do datoteke (Enter za 'kod.txt'): ").strip()
|
| 216 |
+
if putanja == "":
|
| 217 |
+
putanja = "kod.txt"
|
| 218 |
+
kod = ucitaj_iz_datoteke(putanja)
|
| 219 |
+
|
| 220 |
+
else:
|
| 221 |
+
print(" Unesi 1 ili 2.")
|
| 222 |
+
continue
|
| 223 |
+
|
| 224 |
+
if not kod:
|
| 225 |
+
print(" Kod je prazan. Pokušaj ponovno.")
|
| 226 |
+
continue
|
| 227 |
+
|
| 228 |
+
print("\nIzvlačim značajke...")
|
| 229 |
+
znacajke = extract_all_features(kod, model=model, tokenizer=tokenizer)
|
| 230 |
+
ispisi_znacajke(znacajke)
|
| 231 |
+
|
| 232 |
+
odgovor = input("Želiš li analizirati još jedan isječak koda? (da/ne): ")
|
| 233 |
+
if odgovor.strip().lower() not in ("da", "d", "yes", "y"):
|
| 234 |
+
print("\nZatvaram program.")
|
| 235 |
+
break
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
if __name__ == "__main__":
|
| 239 |
+
main()
|
model/classifier.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bdc3f94bdbe63a0d8bd55c7d3372d270c324bf0f62d786bddac83bb3d07fef3d
|
| 3 |
+
size 1218635
|
model/feature_names.pkl
ADDED
|
Binary file (803 Bytes). View file
|
|
|
model/scaler.pkl
ADDED
|
Binary file (1.39 kB). View file
|
|
|
primjeri/1.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This program adds two numbers
|
| 2 |
+
|
| 3 |
+
num1 = 1.5
|
| 4 |
+
num2 = 6.3
|
| 5 |
+
|
| 6 |
+
# Add two numbers
|
| 7 |
+
sum = num1 + num2
|
| 8 |
+
|
| 9 |
+
# Display the sum
|
| 10 |
+
print('The sum of {0} and {1} is {2}'.format(num1, num2, sum))
|
primjeri/11.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This program adds two numbers
|
| 2 |
+
|
| 3 |
+
num1 = 1.5
|
| 4 |
+
num2 = 6.3
|
| 5 |
+
|
| 6 |
+
sum = num1 + num2
|
| 7 |
+
|
| 8 |
+
# Display the sum
|
| 9 |
+
print('The sum of {0} and {1} is {2}'.format(num1, num2, sum))
|
primjeri/ai.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def calculate_standard_deviation(numerical_values: list) -> float:
|
| 2 |
+
"""
|
| 3 |
+
Calculate the standard deviation of a list of numerical values.
|
| 4 |
+
|
| 5 |
+
Standard deviation measures the amount of variation or dispersion
|
| 6 |
+
in a dataset relative to its mean value.
|
| 7 |
+
|
| 8 |
+
Args:
|
| 9 |
+
numerical_values: A list of numeric values to analyze.
|
| 10 |
+
|
| 11 |
+
Returns:
|
| 12 |
+
The standard deviation as a floating point number.
|
| 13 |
+
|
| 14 |
+
Raises:
|
| 15 |
+
ValueError: If the input list is empty or contains fewer than two values.
|
| 16 |
+
TypeError: If the input contains non-numeric values.
|
| 17 |
+
"""
|
| 18 |
+
if not numerical_values:
|
| 19 |
+
raise ValueError("Input list must not be empty.")
|
| 20 |
+
if len(numerical_values) < 2:
|
| 21 |
+
raise ValueError("Standard deviation requires at least two values.")
|
| 22 |
+
if not all(isinstance(value, (int, float)) for value in numerical_values):
|
| 23 |
+
raise TypeError("All values in the input list must be numeric.")
|
| 24 |
+
|
| 25 |
+
total_count = len(numerical_values)
|
| 26 |
+
arithmetic_mean = sum(numerical_values) / total_count
|
| 27 |
+
squared_differences = [(value - arithmetic_mean) ** 2 for value in numerical_values]
|
| 28 |
+
variance_value = sum(squared_differences) / (total_count - 1)
|
| 29 |
+
standard_deviation_result = variance_value ** 0.5
|
| 30 |
+
|
| 31 |
+
return standard_deviation_result
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def find_outliers_using_std_deviation(
|
| 35 |
+
numerical_values: list,
|
| 36 |
+
threshold_multiplier: float = 2.0
|
| 37 |
+
) -> list:
|
| 38 |
+
"""
|
| 39 |
+
Identify outliers in a dataset using standard deviation method.
|
| 40 |
+
|
| 41 |
+
Values that fall more than threshold_multiplier standard deviations
|
| 42 |
+
away from the mean are considered outliers.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
numerical_values: A list of numeric values to analyze.
|
| 46 |
+
threshold_multiplier: The number of standard deviations to use
|
| 47 |
+
as the outlier threshold. Defaults to 2.0.
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
A list of values identified as outliers.
|
| 51 |
+
"""
|
| 52 |
+
if not numerical_values:
|
| 53 |
+
raise ValueError("Input list must not be empty.")
|
| 54 |
+
|
| 55 |
+
arithmetic_mean = sum(numerical_values) / len(numerical_values)
|
| 56 |
+
standard_deviation_value = calculate_standard_deviation(numerical_values)
|
| 57 |
+
lower_bound_value = arithmetic_mean - (threshold_multiplier * standard_deviation_value)
|
| 58 |
+
upper_bound_value = arithmetic_mean + (threshold_multiplier * standard_deviation_value)
|
| 59 |
+
|
| 60 |
+
identified_outliers = [
|
| 61 |
+
value for value in numerical_values
|
| 62 |
+
if value < lower_bound_value or value > upper_bound_value
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
return identified_outliers
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def normalize_numerical_dataset(numerical_values: list) -> list:
|
| 69 |
+
"""
|
| 70 |
+
Normalize a list of numerical values to the range [0, 1].
|
| 71 |
+
|
| 72 |
+
Normalization is performed using min-max scaling, which transforms
|
| 73 |
+
each value proportionally within the original range.
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
numerical_values: A list of numeric values to normalize.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
A list of normalized values in the range [0, 1].
|
| 80 |
+
"""
|
| 81 |
+
if not numerical_values:
|
| 82 |
+
raise ValueError("Input list must not be empty.")
|
| 83 |
+
|
| 84 |
+
minimum_value = min(numerical_values)
|
| 85 |
+
maximum_value = max(numerical_values)
|
| 86 |
+
|
| 87 |
+
if minimum_value == maximum_value:
|
| 88 |
+
return [0.0 for _ in numerical_values]
|
| 89 |
+
|
| 90 |
+
value_range = maximum_value - minimum_value
|
| 91 |
+
normalized_values = [
|
| 92 |
+
(value - minimum_value) / value_range
|
| 93 |
+
for value in numerical_values
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
return normalized_values
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def main_execution_function():
|
| 100 |
+
"""
|
| 101 |
+
Main entry point demonstrating statistical analysis functions.
|
| 102 |
+
"""
|
| 103 |
+
sample_numerical_dataset = [12, 15, 14, 10, 18, 45, 13, 11, 16, 14]
|
| 104 |
+
|
| 105 |
+
calculated_deviation = calculate_standard_deviation(sample_numerical_dataset)
|
| 106 |
+
print(f"Standard deviation: {calculated_deviation:.4f}")
|
| 107 |
+
|
| 108 |
+
detected_outliers = find_outliers_using_std_deviation(sample_numerical_dataset)
|
| 109 |
+
print(f"Detected outliers: {detected_outliers}")
|
| 110 |
+
|
| 111 |
+
normalized_dataset = normalize_numerical_dataset(sample_numerical_dataset)
|
| 112 |
+
print(f"Normalized values: {[round(v, 3) for v in normalized_dataset]}")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
main_execution_function()
|