Karim Krklec commited on
Commit
dcce181
·
0 Parent(s):
Files changed (49) hide show
  1. .gitattributes +1 -0
  2. __pycache__/classifier.cpython-313.pyc +0 -0
  3. __pycache__/feature_extraction.cpython-313.pyc +0 -0
  4. __pycache__/language_config.cpython-313.pyc +0 -0
  5. app.py +252 -0
  6. classifier.py +706 -0
  7. data/dataset.csv +0 -0
  8. download_dataset.py +261 -0
  9. feature_extraction.py +625 -0
  10. frontend/.gitignore +24 -0
  11. frontend/README.md +16 -0
  12. frontend/eslint.config.js +21 -0
  13. frontend/index.html +13 -0
  14. frontend/package-lock.json +1677 -0
  15. frontend/package.json +19 -0
  16. frontend/public/favicon.svg +1 -0
  17. frontend/public/icons.svg +24 -0
  18. frontend/src/App.css +184 -0
  19. frontend/src/App.jsx +62 -0
  20. frontend/src/api/client.js +31 -0
  21. frontend/src/assets/hero.png +0 -0
  22. frontend/src/assets/react.svg +1 -0
  23. frontend/src/assets/vite.svg +1 -0
  24. frontend/src/components/CodeEditor.jsx +105 -0
  25. frontend/src/components/DetailSidebar.jsx +65 -0
  26. frontend/src/components/Dropzone.jsx +46 -0
  27. frontend/src/components/ExplanationPanel.jsx +130 -0
  28. frontend/src/components/FeatureCard.jsx +50 -0
  29. frontend/src/components/Heatmap.jsx +200 -0
  30. frontend/src/components/Navbar.jsx +68 -0
  31. frontend/src/components/ResultGauge.jsx +60 -0
  32. frontend/src/components/ResultsTable.jsx +113 -0
  33. frontend/src/components/primitives.jsx +188 -0
  34. frontend/src/index.css +111 -0
  35. frontend/src/main.jsx +10 -0
  36. frontend/src/screens/AnalyzeScreen.jsx +202 -0
  37. frontend/src/screens/BatchScreen.jsx +138 -0
  38. frontend/src/screens/DetailScreen.jsx +105 -0
  39. frontend/src/screens/SimilarityScreen.jsx +125 -0
  40. frontend/src/styles/tokens.css +53 -0
  41. frontend/vite.config.js +15 -0
  42. language_config.py +281 -0
  43. main.py +239 -0
  44. model/classifier.pkl +3 -0
  45. model/feature_names.pkl +0 -0
  46. model/scaler.pkl +0 -0
  47. primjeri/1.py +10 -0
  48. primjeri/11.py +9 -0
  49. 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()