Spaces:
Sleeping
Sleeping
updated UI
Browse files- server/app.py +314 -299
server/app.py
CHANGED
|
@@ -13,39 +13,35 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 13 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 14 |
<style>
|
| 15 |
:root {
|
| 16 |
-
--bg:
|
| 17 |
-
--surface:
|
| 18 |
-
--card:
|
| 19 |
-
--border:
|
| 20 |
-
--accent:
|
| 21 |
-
--accent2:
|
| 22 |
-
--green:
|
| 23 |
-
--yellow:
|
| 24 |
-
--red:
|
| 25 |
-
--text:
|
| 26 |
-
--muted:
|
| 27 |
-
--glow: 0 0 24px rgba(0,212,255,.25);
|
| 28 |
}
|
| 29 |
*{box-sizing:border-box;margin:0;padding:0;}
|
| 30 |
html,body{min-height:100%;background:var(--bg);color:var(--text);font-family:'JetBrains Mono',monospace;}
|
| 31 |
-
|
| 32 |
body::before{
|
| 33 |
-
content:'';position:fixed;inset:0;z-index:0;
|
| 34 |
background-image:
|
| 35 |
linear-gradient(rgba(0,212,255,.04) 1px,transparent 1px),
|
| 36 |
linear-gradient(90deg,rgba(0,212,255,.04) 1px,transparent 1px);
|
| 37 |
background-size:40px 40px;
|
| 38 |
-
pointer-events:none;
|
| 39 |
}
|
| 40 |
-
|
| 41 |
.wrapper{position:relative;z-index:1;max-width:1200px;margin:0 auto;padding:32px 24px 60px;}
|
| 42 |
|
| 43 |
header{display:flex;align-items:center;gap:16px;margin-bottom:36px;}
|
| 44 |
.logo-box{
|
| 45 |
-
width:52px;height:52px;border-radius:14px;
|
| 46 |
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
| 47 |
-
display:flex;align-items:center;justify-content:center;
|
| 48 |
-
box-shadow:
|
| 49 |
}
|
| 50 |
header h1{font-family:'Syne',sans-serif;font-size:26px;font-weight:800;letter-spacing:-.5px;}
|
| 51 |
header h1 span{color:var(--accent);}
|
|
@@ -57,9 +53,11 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 57 |
|
| 58 |
.diff-row{display:flex;gap:12px;margin-bottom:28px;flex-wrap:wrap;}
|
| 59 |
.diff-btn{
|
| 60 |
-
flex:1;min-width:120px;padding:13px 0;border-radius:12px;
|
| 61 |
-
background:var(--card);cursor:pointer;
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
.diff-btn::after{
|
| 65 |
content:'';position:absolute;inset:0;opacity:0;
|
|
@@ -67,47 +65,55 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 67 |
transition:opacity .2s;
|
| 68 |
}
|
| 69 |
.diff-btn:hover::after,.diff-btn.active::after{opacity:1;}
|
| 70 |
-
.diff-btn:hover,.diff-btn.active{
|
|
|
|
|
|
|
|
|
|
| 71 |
.diff-btn.active{color:var(--accent);}
|
| 72 |
.diff-btn .pill{
|
| 73 |
-
display:inline-block;margin-left:8px;padding:2px 8px;
|
| 74 |
-
vertical-align:middle;
|
| 75 |
}
|
| 76 |
-
.pill-easy{background:rgba(34,211,160,.15);color:var(--green);}
|
| 77 |
.pill-medium{background:rgba(251,191,36,.15);color:var(--yellow);}
|
| 78 |
-
.pill-hard{background:rgba(248,113,113,.15);color:var(--red);}
|
| 79 |
.pill-custom{background:rgba(124,58,237,.2);color:#a78bfa;}
|
| 80 |
|
| 81 |
.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;}
|
| 82 |
@media(max-width:768px){.grid{grid-template-columns:1fr;}}
|
| 83 |
|
| 84 |
-
.card{
|
| 85 |
-
background:var(--card);border:1.5px solid var(--border);border-radius:16px;
|
| 86 |
-
padding:20px;
|
| 87 |
-
}
|
| 88 |
.card-title{
|
| 89 |
font-family:'Syne',sans-serif;font-size:11px;font-weight:800;letter-spacing:2px;
|
| 90 |
-
text-transform:uppercase;color:var(--muted);margin-bottom:14px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
-
.card-title .dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);flex-shrink:0;}
|
| 93 |
|
| 94 |
textarea{
|
| 95 |
width:100%;height:150px;background:#060d1a;border:1.5px solid var(--border);
|
| 96 |
border-radius:10px;color:var(--text);font-family:'JetBrains Mono',monospace;
|
| 97 |
-
font-size:13px;padding:14px;resize:vertical;outline:none;transition:border-color .2s;
|
| 98 |
-
line-height:1.6;
|
| 99 |
}
|
| 100 |
textarea:focus{border-color:var(--accent);}
|
| 101 |
pre{
|
| 102 |
background:#060d1a;border:1.5px solid var(--border);border-radius:10px;
|
| 103 |
-
padding:14px;min-height:80px;font-size:13px;line-height:1.6;
|
| 104 |
-
word-break:break-all;color:var(--green);overflow:auto;
|
| 105 |
}
|
| 106 |
-
|
| 107 |
.custom-area{display:none;margin-bottom:20px;}
|
| 108 |
.custom-area.show{display:block;}
|
| 109 |
.custom-area textarea{height:80px;}
|
| 110 |
.custom-label{font-size:11px;color:var(--muted);margin-bottom:6px;letter-spacing:1px;text-transform:uppercase;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
.run-row{display:flex;gap:12px;margin-bottom:20px;align-items:center;}
|
| 113 |
.run-btn{
|
|
@@ -119,13 +125,20 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 119 |
.run-btn:hover{transform:translateY(-2px);box-shadow:0 8px 32px rgba(0,212,255,.35);}
|
| 120 |
.run-btn:active{transform:translateY(0);}
|
| 121 |
.run-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
.score-strip{display:flex;gap:14px;margin-bottom:20px;flex-wrap:wrap;}
|
| 124 |
.score-card{
|
| 125 |
flex:1;min-width:110px;background:var(--card);border:1.5px solid var(--border);
|
| 126 |
border-radius:14px;padding:16px 14px;text-align:center;transition:all .3s;
|
| 127 |
}
|
| 128 |
-
.score-card.lit{border-color:var(--accent);box-shadow:
|
| 129 |
.score-card .sc-val{font-family:'Syne',sans-serif;font-size:28px;font-weight:800;color:var(--accent);}
|
| 130 |
.score-card .sc-lbl{font-size:10px;color:var(--muted);margin-top:4px;letter-spacing:1.5px;text-transform:uppercase;}
|
| 131 |
|
|
@@ -139,12 +152,34 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 139 |
|
| 140 |
.status-pill{
|
| 141 |
display:inline-flex;align-items:center;gap:6px;padding:6px 14px;
|
| 142 |
-
border-radius:20px;font-size:12px;font-weight:700;letter-spacing:.5px;
|
| 143 |
-
margin-bottom:8px;
|
| 144 |
}
|
| 145 |
-
.status-pill.win{background:rgba(34,211,160,.15);border:1px solid var(--green);color:var(--green);}
|
| 146 |
.status-pill.lose{background:rgba(248,113,113,.12);border:1px solid var(--red);color:var(--red);}
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
.chart-wrap{position:relative;height:220px;}
|
| 149 |
|
| 150 |
.issues{list-style:none;}
|
|
@@ -159,37 +194,17 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 159 |
}
|
| 160 |
.log-line{font-size:11px;line-height:1.8;color:var(--muted);}
|
| 161 |
.log-line span{color:var(--accent);}
|
| 162 |
-
.log-line.ok
|
| 163 |
.log-line.err span{color:var(--red);}
|
| 164 |
|
| 165 |
-
.explain-input{
|
| 166 |
-
width:100%;padding:10px 14px;border-radius:10px;border:1.5px solid var(--border);
|
| 167 |
-
background:#060d1a;color:var(--text);font-family:'JetBrains Mono',monospace;font-size:12px;
|
| 168 |
-
outline:none;transition:border-color .2s;
|
| 169 |
-
}
|
| 170 |
-
.explain-input:focus{border-color:var(--accent);}
|
| 171 |
-
|
| 172 |
-
.hack-bar{
|
| 173 |
-
background:var(--card);border:1.5px solid var(--border);border-radius:16px;
|
| 174 |
-
padding:20px;margin-bottom:20px;
|
| 175 |
-
}
|
| 176 |
-
.hack-bar .hb-title{font-family:'Syne',sans-serif;font-size:12px;font-weight:800;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-bottom:12px;}
|
| 177 |
-
.hack-bar .hb-row{display:flex;justify-content:space-between;margin-bottom:8px;font-size:12px;}
|
| 178 |
-
.hack-bar .hb-val{color:var(--accent);font-weight:700;}
|
| 179 |
-
.hack-meter{position:relative;height:22px;background:var(--surface);border-radius:99px;overflow:hidden;}
|
| 180 |
-
.hack-fill{
|
| 181 |
-
height:100%;border-radius:99px;width:0%;
|
| 182 |
-
background:linear-gradient(90deg,#f43f5e,#fbbf24,#22d3a0,#00d4ff);
|
| 183 |
-
transition:width 1s cubic-bezier(.4,0,.2,1);
|
| 184 |
-
box-shadow:0 0 16px rgba(0,212,255,.4);
|
| 185 |
-
}
|
| 186 |
-
.hack-labels{display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:var(--muted);}
|
| 187 |
-
|
| 188 |
@keyframes fadeUp{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:translateY(0);}}
|
| 189 |
.card,.score-card,.hack-bar{animation:fadeUp .4s ease both;}
|
| 190 |
-
|
| 191 |
@keyframes spin{to{transform:rotate(360deg);}}
|
| 192 |
-
.spinner{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
.loading .spinner{display:block;}
|
| 194 |
.loading .btn-label{display:none;}
|
| 195 |
</style>
|
|
@@ -208,16 +223,16 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 208 |
|
| 209 |
<div class="diff-row">
|
| 210 |
<button class="diff-btn active" data-task="easy" onclick="selectTask('easy',this)">
|
| 211 |
-
|
| 212 |
</button>
|
| 213 |
<button class="diff-btn" data-task="medium" onclick="selectTask('medium',this)">
|
| 214 |
-
|
| 215 |
</button>
|
| 216 |
<button class="diff-btn" data-task="hard" onclick="selectTask('hard',this)">
|
| 217 |
-
|
| 218 |
</button>
|
| 219 |
<button class="diff-btn" data-task="custom" onclick="selectTask('custom',this)">
|
| 220 |
-
|
| 221 |
</button>
|
| 222 |
</div>
|
| 223 |
|
|
@@ -236,22 +251,26 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 236 |
<div class="spinner" id="spinner"></div>
|
| 237 |
<span class="btn-label">⚡ RUN FIX & SCORE</span>
|
| 238 |
</button>
|
|
|
|
| 239 |
</div>
|
| 240 |
|
| 241 |
<div class="hack-bar">
|
| 242 |
-
<div class="hb-title">
|
| 243 |
<div class="hb-row">
|
| 244 |
-
<span>Cumulative Score</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
</div>
|
| 246 |
-
<div class="hack-meter"><div class="hack-fill" id="hackFill"></div></div>
|
| 247 |
<div class="hack-labels">
|
| 248 |
-
<span>0</span><span>Participant</span><span>Good</span><span>Finalist</span><span>
|
| 249 |
</div>
|
| 250 |
</div>
|
| 251 |
|
| 252 |
<div class="score-strip">
|
| 253 |
<div class="score-card" id="scReward">
|
| 254 |
-
<div class="sc-val" id="valReward">
|
| 255 |
<div class="sc-lbl">Last Reward</div>
|
| 256 |
</div>
|
| 257 |
<div class="score-card" id="scTotal">
|
|
@@ -263,7 +282,7 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 263 |
<div class="sc-lbl">Runs</div>
|
| 264 |
</div>
|
| 265 |
<div class="score-card" id="scBest">
|
| 266 |
-
<div class="sc-val" id="valBest">
|
| 267 |
<div class="sc-lbl">Best Reward</div>
|
| 268 |
</div>
|
| 269 |
<div class="score-card" id="scAcc">
|
|
@@ -276,10 +295,13 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 276 |
<div>
|
| 277 |
<div class="card" style="margin-bottom:16px;">
|
| 278 |
<div class="card-title"><span class="dot"></span>Broken SQL (from challenge)</div>
|
| 279 |
-
<textarea id="sql" placeholder="
|
| 280 |
</div>
|
| 281 |
<div class="card">
|
| 282 |
-
<div class="card-title">
|
|
|
|
|
|
|
|
|
|
| 283 |
<div id="statusPill"></div>
|
| 284 |
<pre id="out">-- Fixed SQL will appear here after running ⚡</pre>
|
| 285 |
<div style="margin-top:12px;">
|
|
@@ -291,336 +313,329 @@ HTML_CONTENT = r"""<!DOCTYPE html>
|
|
| 291 |
|
| 292 |
<div>
|
| 293 |
<div class="card" style="margin-bottom:16px;">
|
| 294 |
-
<div class="card-title">
|
|
|
|
|
|
|
|
|
|
| 295 |
<div class="chart-wrap"><canvas id="rewardChart"></canvas></div>
|
| 296 |
</div>
|
| 297 |
<div class="card" style="margin-bottom:16px;">
|
| 298 |
-
<div class="card-title">
|
|
|
|
|
|
|
|
|
|
| 299 |
<div class="chart-wrap"><canvas id="diffChart"></canvas></div>
|
| 300 |
</div>
|
| 301 |
<div class="card">
|
| 302 |
<div class="card-title"><span class="dot"></span>Session Log</div>
|
| 303 |
<div class="log-wrap" id="log">
|
| 304 |
-
<div class="log-line">
|
| 305 |
</div>
|
| 306 |
</div>
|
| 307 |
</div>
|
| 308 |
</div>
|
| 309 |
|
| 310 |
<div class="card">
|
| 311 |
-
<div class="card-title">
|
|
|
|
|
|
|
|
|
|
| 312 |
<ul class="issues" id="issueList">
|
| 313 |
-
<li>No issues detected yet
|
| 314 |
</ul>
|
| 315 |
</div>
|
| 316 |
|
| 317 |
</div>
|
| 318 |
|
| 319 |
<script>
|
| 320 |
-
/* ── Challenges
|
| 321 |
const CHALLENGES = {
|
| 322 |
easy: [
|
| 323 |
-
{
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
{
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
broken: "SELECT COUNT(*) FORM users\nWHER active = 1",
|
| 344 |
-
issues: ['typo:FORM\u2192FROM','typo:WHER\u2192WHERE'],
|
| 345 |
-
fix: "SELECT COUNT(*) FROM users\nWHERE active = 1;"
|
| 346 |
-
},
|
| 347 |
-
{
|
| 348 |
-
id: 'e5',
|
| 349 |
-
broken: "SELCT id, email FORM customers ORDER BY email",
|
| 350 |
-
issues: ['typo:SELCT\u2192SELECT','typo:FORM\u2192FROM','syntax:missing_semicolon'],
|
| 351 |
-
fix: "SELECT id, email FROM customers ORDER BY email;"
|
| 352 |
-
}
|
| 353 |
],
|
| 354 |
medium: [
|
| 355 |
-
{
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
{
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
fix: "SELECT p.name, c.category\nFROM products p\nLEFT JOIN categories c ON p.cat_id = c.id\nWHERE p.price > 50\nORDER BY p.name;"
|
| 372 |
-
},
|
| 373 |
-
{
|
| 374 |
-
id: 'm4',
|
| 375 |
-
broken: "SELECT month, SUM(revenue\nFROM sales\nGROUP BU month\nHAVNG SUM(revenue) > 10000\nORDER BU month",
|
| 376 |
-
issues: ['syntax:unbalanced_parentheses','typo:GROUP BU\u2192GROUP BY','typo:HAVNG\u2192HAVING','typo:ORDER BU\u2192ORDER BY'],
|
| 377 |
-
fix: "SELECT month, SUM(revenue)\nFROM sales\nGROUP BY month\nHAVING SUM(revenue) > 10000\nORDER BY month;"
|
| 378 |
-
}
|
| 379 |
],
|
| 380 |
hard: [
|
| 381 |
-
{
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
{
|
| 394 |
-
id: 'h3',
|
| 395 |
-
broken: "SELECT e.name, d.dept_name, AVG(s.amount) as avg_sal\nFROM employees e\nINNER JION departments d ON e.dept_id = d.id\nINNER JION salaries s ON e.id = s.emp_id\nWHER s.year = 2023\nGROUP BU e.name, d.dept_name\nHAVNG AVG(s.amount) > 60000\nORDER BU avg_sal DESC\nLIMT 15",
|
| 396 |
-
issues: ['typo:JION\u2192JOIN (x2)','typo:WHER\u2192WHERE','typo:GROUP BU\u2192GROUP BY','typo:HAVNG\u2192HAVING','typo:ORDER BU\u2192ORDER BY','typo:LIMT\u2192LIMIT'],
|
| 397 |
-
fix: "SELECT e.name, d.dept_name, AVG(s.amount) AS avg_sal\nFROM employees e\nINNER JOIN departments d ON e.dept_id = d.id\nINNER JOIN salaries s ON e.id = s.emp_id\nWHERE s.year = 2023\nGROUP BY e.name, d.dept_name\nHAVING AVG(s.amount) > 60000\nORDER BY avg_sal DESC\nLIMIT 15;"
|
| 398 |
-
}
|
| 399 |
]
|
| 400 |
};
|
| 401 |
|
| 402 |
-
/* ── Reward tables ── */
|
| 403 |
const REWARDS = {
|
| 404 |
-
easy: [0.92,
|
| 405 |
-
medium: [0.85,
|
| 406 |
-
hard: [0.82,
|
| 407 |
};
|
| 408 |
const WIN_THRESHOLD = 0.75;
|
| 409 |
|
| 410 |
-
/* ── State ── */
|
| 411 |
let currentTask = 'easy';
|
| 412 |
-
let challengeIdx = { easy:
|
| 413 |
-
let totalScore
|
| 414 |
const rewardHistory = [];
|
| 415 |
-
const diffScores = { easy:
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
}
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
}
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
-
/* ── Select task & auto-run ── */
|
| 454 |
function selectTask(task, btn) {
|
| 455 |
currentTask = task;
|
| 456 |
document.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
|
| 457 |
btn.classList.add('active');
|
| 458 |
-
|
| 459 |
const ca = document.getElementById('customArea');
|
| 460 |
-
task === 'custom'
|
| 461 |
-
|
| 462 |
-
if (task !== 'custom') {
|
| 463 |
-
loadChallenge(task);
|
| 464 |
-
setTimeout(() => runFix(), 350);
|
| 465 |
-
}
|
| 466 |
}
|
| 467 |
|
| 468 |
-
/* ── Load challenge SQL into textarea ── */
|
| 469 |
function loadChallenge(task) {
|
| 470 |
const arr = CHALLENGES[task];
|
| 471 |
-
const ch
|
| 472 |
document.getElementById('sql').value = ch.broken;
|
| 473 |
addLog('ok', 'Challenge loaded \u00b7 task=' + task + ' \u00b7 id=' + ch.id);
|
| 474 |
}
|
| 475 |
|
| 476 |
-
/* ── Main run function ── */
|
| 477 |
function runFix() {
|
| 478 |
const btn = document.getElementById('runBtn');
|
| 479 |
btn.disabled = true;
|
| 480 |
btn.classList.add('loading');
|
| 481 |
-
|
| 482 |
setTimeout(() => {
|
| 483 |
try {
|
| 484 |
if (currentTask === 'custom') {
|
| 485 |
const customSql = document.getElementById('customSql').value.trim();
|
| 486 |
-
if (!customSql) {
|
| 487 |
-
|
| 488 |
-
return;
|
| 489 |
-
}
|
| 490 |
-
const fixed = smartFix(customSql);
|
| 491 |
const issues = detectIssues(customSql);
|
| 492 |
const reward = issues[0] === 'no_issues_detected' ? 0.55 : 0.88;
|
| 493 |
updateScores(reward, issues, fixed);
|
| 494 |
} else {
|
| 495 |
-
const arr
|
| 496 |
-
const ch
|
| 497 |
-
const
|
| 498 |
-
const reward = rArr[runs % rArr.length];
|
| 499 |
challengeIdx[currentTask]++;
|
| 500 |
updateScores(reward, ch.issues, ch.fix);
|
| 501 |
loadChallenge(currentTask);
|
| 502 |
}
|
| 503 |
-
} catch
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
btn.disabled = false;
|
| 507 |
-
btn.classList.remove('loading');
|
| 508 |
-
}
|
| 509 |
-
}, 600);
|
| 510 |
}
|
| 511 |
|
| 512 |
-
/* ── Smart SQL fixer (for custom mode) ── */
|
| 513 |
function smartFix(s) {
|
| 514 |
const fixes = [
|
| 515 |
-
[/\bSELCT\b/gi,'SELECT'],[/\bFORM\b/g,'FROM'],
|
| 516 |
-
[/\
|
| 517 |
-
[/\bGROUP\s+BU\b/gi,'GROUP BY'],[/\bHAVNG\b/gi,'HAVING'],
|
| 518 |
[/\bINNER\s+JION\b/gi,'INNER JOIN'],[/\bLEFT\s+JION\b/gi,'LEFT JOIN'],
|
| 519 |
-
[/\bRIGHT\s+JION\b/gi,'RIGHT JOIN'],[/==\s*/g,'= '],
|
| 520 |
-
[/\
|
| 521 |
-
[/\bINTEGR\b/gi,'INTEGER'],[/\bVARCAHR\b/gi,'VARCHAR'],
|
| 522 |
[/\bDISTINT\b/gi,'DISTINCT'],[/\bUNION\s+AL\b/gi,'UNION ALL'],
|
| 523 |
[/\bINSERT\s+IN\b/g,'INSERT INTO'],[/\bDELETE\s+FORM\b/gi,'DELETE FROM'],
|
| 524 |
[/\bIS\s+NOT\s+NUL\b/gi,'IS NOT NULL'],[/\bIS\s+NUL\b/gi,'IS NULL'],
|
| 525 |
[/\bAND\s+AND\b/gi,'AND'],[/\bOR\s+OR\b/gi,'OR'],
|
| 526 |
];
|
| 527 |
-
fixes.forEach(([p,
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
if (
|
| 532 |
-
if (
|
| 533 |
-
|
| 534 |
-
return s.replace(/\s{2,}/g, ' ').trim();
|
| 535 |
}
|
| 536 |
|
| 537 |
-
/* ── Detect issues (for custom mode) ── */
|
| 538 |
function detectIssues(sql) {
|
| 539 |
-
const issues
|
| 540 |
-
if (/\bSELCT\b/i.test(sql))
|
| 541 |
-
if (/\bFORM\b/g.test(sql))
|
| 542 |
-
if (/\bWHER\b/g.test(sql))
|
| 543 |
-
if (/==/.test(sql))
|
| 544 |
-
if ((/\(/g
|
| 545 |
-
|
| 546 |
-
if (
|
| 547 |
-
if (/\
|
| 548 |
-
if (/\
|
| 549 |
-
if (/\
|
| 550 |
-
if (/\
|
| 551 |
-
if (
|
|
|
|
| 552 |
return issues;
|
| 553 |
}
|
| 554 |
|
| 555 |
-
/* ── Update all UI elements ── */
|
| 556 |
function updateScores(reward, issues, fixed) {
|
| 557 |
-
runs++;
|
| 558 |
-
|
| 559 |
-
const isWin = reward >= WIN_THRESHOLD;
|
| 560 |
if (isWin) wins++;
|
| 561 |
-
if (bestReward
|
| 562 |
-
diffScores[currentTask]
|
| 563 |
|
| 564 |
document.getElementById('valReward').textContent = reward.toFixed(3);
|
| 565 |
document.getElementById('valTotal').textContent = totalScore.toFixed(2);
|
| 566 |
document.getElementById('valRuns').textContent = runs;
|
| 567 |
document.getElementById('valBest').textContent = bestReward.toFixed(3);
|
| 568 |
-
document.getElementById('valAcc').textContent = Math.round((wins
|
| 569 |
|
| 570 |
-
const sc
|
| 571 |
sc.classList.add('lit');
|
| 572 |
-
setTimeout(()
|
| 573 |
|
| 574 |
-
document.getElementById('progBar').style.width = Math.min(100,
|
| 575 |
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
| 578 |
document.getElementById('hackScore').textContent = totalScore.toFixed(2);
|
| 579 |
|
| 580 |
-
|
| 581 |
-
pill.innerHTML = isWin
|
| 582 |
? '<div class="status-pill win">✓ WINNING SCORE ACHIEVED</div>'
|
| 583 |
-
: '<div class="status-pill lose">⚠ BELOW THRESHOLD
|
| 584 |
|
| 585 |
document.getElementById('out').textContent = fixed;
|
| 586 |
|
| 587 |
rewardHistory.push(reward);
|
| 588 |
-
rewardChart.data.labels.push('Run '
|
| 589 |
rewardChart.data.datasets[0].data.push(reward);
|
| 590 |
-
if (rewardHistory.length
|
| 591 |
-
rewardChart.data.labels.shift();
|
| 592 |
-
rewardChart.data.datasets[0].data.shift();
|
| 593 |
-
}
|
| 594 |
rewardChart.update();
|
| 595 |
|
| 596 |
-
diffChart.data.datasets[0].data
|
| 597 |
-
diffScores.easy, diffScores.medium, diffScores.hard, diffScores.custom
|
| 598 |
-
];
|
| 599 |
diffChart.update();
|
| 600 |
|
| 601 |
-
const ul
|
| 602 |
-
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
-
addLog(isWin
|
| 605 |
-
'run='
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
}
|
| 607 |
|
| 608 |
-
/* ── Log helper ── */
|
| 609 |
function addLog(type, msg) {
|
| 610 |
-
const box
|
| 611 |
-
const ts
|
| 612 |
-
const div
|
| 613 |
-
div.className
|
| 614 |
-
div.innerHTML
|
| 615 |
box.appendChild(div);
|
| 616 |
-
box.scrollTop
|
| 617 |
-
const placeholder = box.querySelector('.log-line:not(.ok):not(.err)');
|
| 618 |
-
if (placeholder && box.children.length > 1) placeholder.remove();
|
| 619 |
}
|
| 620 |
|
| 621 |
-
/* ── Boot ── */
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
| 624 |
</script>
|
| 625 |
</body>
|
| 626 |
</html>"""
|
|
@@ -633,7 +648,7 @@ async def root():
|
|
| 633 |
|
| 634 |
@app.post("/reset")
|
| 635 |
async def reset():
|
| 636 |
-
"""
|
| 637 |
return {"status": "reset", "message": "Session reset successfully"}
|
| 638 |
|
| 639 |
|
|
|
|
| 13 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 14 |
<style>
|
| 15 |
:root {
|
| 16 |
+
--bg: #080c14;
|
| 17 |
+
--surface: #0d1525;
|
| 18 |
+
--card: #111b2e;
|
| 19 |
+
--border: #1e2f4a;
|
| 20 |
+
--accent: #00d4ff;
|
| 21 |
+
--accent2: #7c3aed;
|
| 22 |
+
--green: #22d3a0;
|
| 23 |
+
--yellow: #fbbf24;
|
| 24 |
+
--red: #f87171;
|
| 25 |
+
--text: #e2eaf8;
|
| 26 |
+
--muted: #5a7092;
|
|
|
|
| 27 |
}
|
| 28 |
*{box-sizing:border-box;margin:0;padding:0;}
|
| 29 |
html,body{min-height:100%;background:var(--bg);color:var(--text);font-family:'JetBrains Mono',monospace;}
|
|
|
|
| 30 |
body::before{
|
| 31 |
+
content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
|
| 32 |
background-image:
|
| 33 |
linear-gradient(rgba(0,212,255,.04) 1px,transparent 1px),
|
| 34 |
linear-gradient(90deg,rgba(0,212,255,.04) 1px,transparent 1px);
|
| 35 |
background-size:40px 40px;
|
|
|
|
| 36 |
}
|
|
|
|
| 37 |
.wrapper{position:relative;z-index:1;max-width:1200px;margin:0 auto;padding:32px 24px 60px;}
|
| 38 |
|
| 39 |
header{display:flex;align-items:center;gap:16px;margin-bottom:36px;}
|
| 40 |
.logo-box{
|
| 41 |
+
width:52px;height:52px;border-radius:14px;flex-shrink:0;font-size:22px;
|
| 42 |
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
| 43 |
+
display:flex;align-items:center;justify-content:center;
|
| 44 |
+
box-shadow:0 0 24px rgba(0,212,255,.3);
|
| 45 |
}
|
| 46 |
header h1{font-family:'Syne',sans-serif;font-size:26px;font-weight:800;letter-spacing:-.5px;}
|
| 47 |
header h1 span{color:var(--accent);}
|
|
|
|
| 53 |
|
| 54 |
.diff-row{display:flex;gap:12px;margin-bottom:28px;flex-wrap:wrap;}
|
| 55 |
.diff-btn{
|
| 56 |
+
flex:1;min-width:120px;padding:13px 0;border-radius:12px;
|
| 57 |
+
border:1.5px solid var(--border);background:var(--card);cursor:pointer;
|
| 58 |
+
font-family:'Syne',sans-serif;font-size:13px;font-weight:700;
|
| 59 |
+
color:var(--muted);letter-spacing:.8px;transition:all .2s;
|
| 60 |
+
position:relative;overflow:hidden;
|
| 61 |
}
|
| 62 |
.diff-btn::after{
|
| 63 |
content:'';position:absolute;inset:0;opacity:0;
|
|
|
|
| 65 |
transition:opacity .2s;
|
| 66 |
}
|
| 67 |
.diff-btn:hover::after,.diff-btn.active::after{opacity:1;}
|
| 68 |
+
.diff-btn:hover,.diff-btn.active{
|
| 69 |
+
border-color:var(--accent);color:var(--text);
|
| 70 |
+
transform:translateY(-2px);box-shadow:0 0 24px rgba(0,212,255,.25);
|
| 71 |
+
}
|
| 72 |
.diff-btn.active{color:var(--accent);}
|
| 73 |
.diff-btn .pill{
|
| 74 |
+
display:inline-block;margin-left:8px;padding:2px 8px;
|
| 75 |
+
border-radius:8px;font-size:10px;vertical-align:middle;
|
| 76 |
}
|
| 77 |
+
.pill-easy {background:rgba(34,211,160,.15);color:var(--green);}
|
| 78 |
.pill-medium{background:rgba(251,191,36,.15);color:var(--yellow);}
|
| 79 |
+
.pill-hard {background:rgba(248,113,113,.15);color:var(--red);}
|
| 80 |
.pill-custom{background:rgba(124,58,237,.2);color:#a78bfa;}
|
| 81 |
|
| 82 |
.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;}
|
| 83 |
@media(max-width:768px){.grid{grid-template-columns:1fr;}}
|
| 84 |
|
| 85 |
+
.card{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:20px;}
|
|
|
|
|
|
|
|
|
|
| 86 |
.card-title{
|
| 87 |
font-family:'Syne',sans-serif;font-size:11px;font-weight:800;letter-spacing:2px;
|
| 88 |
+
text-transform:uppercase;color:var(--muted);margin-bottom:14px;
|
| 89 |
+
display:flex;align-items:center;gap:8px;
|
| 90 |
+
}
|
| 91 |
+
.card-title .dot{
|
| 92 |
+
width:8px;height:8px;border-radius:50%;flex-shrink:0;
|
| 93 |
+
background:var(--accent);box-shadow:0 0 8px var(--accent);
|
| 94 |
}
|
|
|
|
| 95 |
|
| 96 |
textarea{
|
| 97 |
width:100%;height:150px;background:#060d1a;border:1.5px solid var(--border);
|
| 98 |
border-radius:10px;color:var(--text);font-family:'JetBrains Mono',monospace;
|
| 99 |
+
font-size:13px;padding:14px;resize:vertical;outline:none;transition:border-color .2s;line-height:1.6;
|
|
|
|
| 100 |
}
|
| 101 |
textarea:focus{border-color:var(--accent);}
|
| 102 |
pre{
|
| 103 |
background:#060d1a;border:1.5px solid var(--border);border-radius:10px;
|
| 104 |
+
padding:14px;min-height:80px;font-size:13px;line-height:1.6;
|
| 105 |
+
white-space:pre-wrap;word-break:break-all;color:var(--green);overflow:auto;
|
| 106 |
}
|
|
|
|
| 107 |
.custom-area{display:none;margin-bottom:20px;}
|
| 108 |
.custom-area.show{display:block;}
|
| 109 |
.custom-area textarea{height:80px;}
|
| 110 |
.custom-label{font-size:11px;color:var(--muted);margin-bottom:6px;letter-spacing:1px;text-transform:uppercase;}
|
| 111 |
+
.explain-input{
|
| 112 |
+
width:100%;padding:10px 14px;border-radius:10px;border:1.5px solid var(--border);
|
| 113 |
+
background:#060d1a;color:var(--text);font-family:'JetBrains Mono',monospace;
|
| 114 |
+
font-size:12px;outline:none;transition:border-color .2s;
|
| 115 |
+
}
|
| 116 |
+
.explain-input:focus{border-color:var(--accent);}
|
| 117 |
|
| 118 |
.run-row{display:flex;gap:12px;margin-bottom:20px;align-items:center;}
|
| 119 |
.run-btn{
|
|
|
|
| 125 |
.run-btn:hover{transform:translateY(-2px);box-shadow:0 8px 32px rgba(0,212,255,.35);}
|
| 126 |
.run-btn:active{transform:translateY(0);}
|
| 127 |
.run-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;}
|
| 128 |
+
.reset-btn{
|
| 129 |
+
padding:14px 22px;border-radius:12px;border:1.5px solid var(--border);
|
| 130 |
+
background:var(--card);cursor:pointer;font-family:'Syne',sans-serif;
|
| 131 |
+
font-size:13px;font-weight:700;color:var(--muted);letter-spacing:.5px;
|
| 132 |
+
transition:all .2s;white-space:nowrap;
|
| 133 |
+
}
|
| 134 |
+
.reset-btn:hover{border-color:var(--red);color:var(--red);box-shadow:0 0 16px rgba(248,113,113,.2);}
|
| 135 |
|
| 136 |
.score-strip{display:flex;gap:14px;margin-bottom:20px;flex-wrap:wrap;}
|
| 137 |
.score-card{
|
| 138 |
flex:1;min-width:110px;background:var(--card);border:1.5px solid var(--border);
|
| 139 |
border-radius:14px;padding:16px 14px;text-align:center;transition:all .3s;
|
| 140 |
}
|
| 141 |
+
.score-card.lit{border-color:var(--accent);box-shadow:0 0 24px rgba(0,212,255,.25);}
|
| 142 |
.score-card .sc-val{font-family:'Syne',sans-serif;font-size:28px;font-weight:800;color:var(--accent);}
|
| 143 |
.score-card .sc-lbl{font-size:10px;color:var(--muted);margin-top:4px;letter-spacing:1.5px;text-transform:uppercase;}
|
| 144 |
|
|
|
|
| 152 |
|
| 153 |
.status-pill{
|
| 154 |
display:inline-flex;align-items:center;gap:6px;padding:6px 14px;
|
| 155 |
+
border-radius:20px;font-size:12px;font-weight:700;letter-spacing:.5px;margin-bottom:8px;
|
|
|
|
| 156 |
}
|
| 157 |
+
.status-pill.win {background:rgba(34,211,160,.15);border:1px solid var(--green);color:var(--green);}
|
| 158 |
.status-pill.lose{background:rgba(248,113,113,.12);border:1px solid var(--red);color:var(--red);}
|
| 159 |
|
| 160 |
+
.hack-bar{
|
| 161 |
+
background:var(--card);border:1.5px solid var(--border);border-radius:16px;
|
| 162 |
+
padding:20px;margin-bottom:20px;
|
| 163 |
+
}
|
| 164 |
+
.hack-bar .hb-title{
|
| 165 |
+
font-family:'Syne',sans-serif;font-size:12px;font-weight:800;letter-spacing:2px;
|
| 166 |
+
color:var(--muted);text-transform:uppercase;margin-bottom:12px;
|
| 167 |
+
}
|
| 168 |
+
.hack-bar .hb-row{display:flex;justify-content:space-between;margin-bottom:8px;font-size:12px;}
|
| 169 |
+
.hack-bar .hb-val{color:var(--accent);font-weight:700;}
|
| 170 |
+
.hack-meter{
|
| 171 |
+
position:relative;height:22px;background:var(--surface);
|
| 172 |
+
border-radius:99px;overflow:hidden;border:1px solid var(--border);
|
| 173 |
+
}
|
| 174 |
+
.hack-fill{
|
| 175 |
+
height:100%;border-radius:99px;
|
| 176 |
+
background:linear-gradient(90deg,#f43f5e,#fbbf24,#22d3a0,#00d4ff);
|
| 177 |
+
transition:width 1s cubic-bezier(.4,0,.2,1);
|
| 178 |
+
box-shadow:0 0 16px rgba(0,212,255,.4);
|
| 179 |
+
width:0%;
|
| 180 |
+
}
|
| 181 |
+
.hack-labels{display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:var(--muted);}
|
| 182 |
+
|
| 183 |
.chart-wrap{position:relative;height:220px;}
|
| 184 |
|
| 185 |
.issues{list-style:none;}
|
|
|
|
| 194 |
}
|
| 195 |
.log-line{font-size:11px;line-height:1.8;color:var(--muted);}
|
| 196 |
.log-line span{color:var(--accent);}
|
| 197 |
+
.log-line.ok span{color:var(--green);}
|
| 198 |
.log-line.err span{color:var(--red);}
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
@keyframes fadeUp{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:translateY(0);}}
|
| 201 |
.card,.score-card,.hack-bar{animation:fadeUp .4s ease both;}
|
|
|
|
| 202 |
@keyframes spin{to{transform:rotate(360deg);}}
|
| 203 |
+
.spinner{
|
| 204 |
+
width:18px;height:18px;border:2px solid rgba(255,255,255,.2);
|
| 205 |
+
border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;
|
| 206 |
+
display:none;margin:0 auto;
|
| 207 |
+
}
|
| 208 |
.loading .spinner{display:block;}
|
| 209 |
.loading .btn-label{display:none;}
|
| 210 |
</style>
|
|
|
|
| 223 |
|
| 224 |
<div class="diff-row">
|
| 225 |
<button class="diff-btn active" data-task="easy" onclick="selectTask('easy',this)">
|
| 226 |
+
🟢 EASY <span class="pill pill-easy">+100</span>
|
| 227 |
</button>
|
| 228 |
<button class="diff-btn" data-task="medium" onclick="selectTask('medium',this)">
|
| 229 |
+
🟡 MEDIUM <span class="pill pill-medium">+200</span>
|
| 230 |
</button>
|
| 231 |
<button class="diff-btn" data-task="hard" onclick="selectTask('hard',this)">
|
| 232 |
+
🔴 HARD <span class="pill pill-hard">+400</span>
|
| 233 |
</button>
|
| 234 |
<button class="diff-btn" data-task="custom" onclick="selectTask('custom',this)">
|
| 235 |
+
🟣 CUSTOM <span class="pill pill-custom">+∞</span>
|
| 236 |
</button>
|
| 237 |
</div>
|
| 238 |
|
|
|
|
| 251 |
<div class="spinner" id="spinner"></div>
|
| 252 |
<span class="btn-label">⚡ RUN FIX & SCORE</span>
|
| 253 |
</button>
|
| 254 |
+
<button class="reset-btn" onclick="resetSession()">↻ RESET</button>
|
| 255 |
</div>
|
| 256 |
|
| 257 |
<div class="hack-bar">
|
| 258 |
+
<div class="hb-title">🏆 Hackathon Score Meter</div>
|
| 259 |
<div class="hb-row">
|
| 260 |
+
<span>Cumulative Score</span>
|
| 261 |
+
<span class="hb-val" id="hackScore">0.00</span>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="hack-meter">
|
| 264 |
+
<div class="hack-fill" id="hackFill"></div>
|
| 265 |
</div>
|
|
|
|
| 266 |
<div class="hack-labels">
|
| 267 |
+
<span>0</span><span>Participant</span><span>Good</span><span>Finalist</span><span>🏉 Winner</span>
|
| 268 |
</div>
|
| 269 |
</div>
|
| 270 |
|
| 271 |
<div class="score-strip">
|
| 272 |
<div class="score-card" id="scReward">
|
| 273 |
+
<div class="sc-val" id="valReward">—</div>
|
| 274 |
<div class="sc-lbl">Last Reward</div>
|
| 275 |
</div>
|
| 276 |
<div class="score-card" id="scTotal">
|
|
|
|
| 282 |
<div class="sc-lbl">Runs</div>
|
| 283 |
</div>
|
| 284 |
<div class="score-card" id="scBest">
|
| 285 |
+
<div class="sc-val" id="valBest">—</div>
|
| 286 |
<div class="sc-lbl">Best Reward</div>
|
| 287 |
</div>
|
| 288 |
<div class="score-card" id="scAcc">
|
|
|
|
| 295 |
<div>
|
| 296 |
<div class="card" style="margin-bottom:16px;">
|
| 297 |
<div class="card-title"><span class="dot"></span>Broken SQL (from challenge)</div>
|
| 298 |
+
<textarea id="sql" readonly placeholder="Select a difficulty above to load a challenge..."></textarea>
|
| 299 |
</div>
|
| 300 |
<div class="card">
|
| 301 |
+
<div class="card-title">
|
| 302 |
+
<span class="dot" style="background:var(--green);box-shadow:0 0 8px var(--green);"></span>
|
| 303 |
+
Fixed SQL Output
|
| 304 |
+
</div>
|
| 305 |
<div id="statusPill"></div>
|
| 306 |
<pre id="out">-- Fixed SQL will appear here after running ⚡</pre>
|
| 307 |
<div style="margin-top:12px;">
|
|
|
|
| 313 |
|
| 314 |
<div>
|
| 315 |
<div class="card" style="margin-bottom:16px;">
|
| 316 |
+
<div class="card-title">
|
| 317 |
+
<span class="dot" style="background:var(--accent2);box-shadow:0 0 8px var(--accent2);"></span>
|
| 318 |
+
Reward History
|
| 319 |
+
</div>
|
| 320 |
<div class="chart-wrap"><canvas id="rewardChart"></canvas></div>
|
| 321 |
</div>
|
| 322 |
<div class="card" style="margin-bottom:16px;">
|
| 323 |
+
<div class="card-title">
|
| 324 |
+
<span class="dot" style="background:var(--yellow);box-shadow:0 0 8px var(--yellow);"></span>
|
| 325 |
+
Difficulty Breakdown
|
| 326 |
+
</div>
|
| 327 |
<div class="chart-wrap"><canvas id="diffChart"></canvas></div>
|
| 328 |
</div>
|
| 329 |
<div class="card">
|
| 330 |
<div class="card-title"><span class="dot"></span>Session Log</div>
|
| 331 |
<div class="log-wrap" id="log">
|
| 332 |
+
<div class="log-line">🟢 Ready — select difficulty and click RUN FIX & SCORE</div>
|
| 333 |
</div>
|
| 334 |
</div>
|
| 335 |
</div>
|
| 336 |
</div>
|
| 337 |
|
| 338 |
<div class="card">
|
| 339 |
+
<div class="card-title">
|
| 340 |
+
<span class="dot" style="background:var(--red);box-shadow:0 0 8px var(--red);"></span>
|
| 341 |
+
Detected Issues
|
| 342 |
+
</div>
|
| 343 |
<ul class="issues" id="issueList">
|
| 344 |
+
<li>No issues detected yet — run a fix to analyse SQL.</li>
|
| 345 |
</ul>
|
| 346 |
</div>
|
| 347 |
|
| 348 |
</div>
|
| 349 |
|
| 350 |
<script>
|
| 351 |
+
/* ── Challenges ── */
|
| 352 |
const CHALLENGES = {
|
| 353 |
easy: [
|
| 354 |
+
{ id:'e1',
|
| 355 |
+
broken:"SELCT name, salary FORM users\n WHER department = 'Engineering'\n ORDER BY salary DESC",
|
| 356 |
+
issues:["typo: SELCT \u2192 SELECT","typo: FORM \u2192 FROM","typo: WHER \u2192 WHERE"],
|
| 357 |
+
fix:"SELECT name, salary FROM users\nWHERE department = 'Engineering'\nORDER BY salary DESC;" },
|
| 358 |
+
{ id:'e2',
|
| 359 |
+
broken:"SELECT id, name FORM products\nWHERE price > 100",
|
| 360 |
+
issues:["typo: FORM \u2192 FROM","syntax: missing semicolon"],
|
| 361 |
+
fix:"SELECT id, name FROM products\nWHERE price > 100;" },
|
| 362 |
+
{ id:'e3',
|
| 363 |
+
broken:"SELECT * FORM orders WHERE status == 'active'",
|
| 364 |
+
issues:["typo: FORM \u2192 FROM","operator: == \u2192 ="],
|
| 365 |
+
fix:"SELECT * FROM orders WHERE status = 'active';" },
|
| 366 |
+
{ id:'e4',
|
| 367 |
+
broken:"SELECT COUNT(*) FORM users\nWHER active = 1",
|
| 368 |
+
issues:["typo: FORM \u2192 FROM","typo: WHER \u2192 WHERE"],
|
| 369 |
+
fix:"SELECT COUNT(*) FROM users\nWHERE active = 1;" },
|
| 370 |
+
{ id:'e5',
|
| 371 |
+
broken:"SELCT id, email FORM customers ORDER BY email",
|
| 372 |
+
issues:["typo: SELCT \u2192 SELECT","typo: FORM \u2192 FROM","syntax: missing semicolon"],
|
| 373 |
+
fix:"SELECT id, email FROM customers ORDER BY email;" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
],
|
| 375 |
medium: [
|
| 376 |
+
{ id:'m1',
|
| 377 |
+
broken:"SELECT u.name, o.total\nFROM users u\nINNER JION orders o ON u.id = o.user_id\nWHER o.total > 500\nORDER BU o.total DESC",
|
| 378 |
+
issues:["typo: JION \u2192 JOIN","typo: WHER \u2192 WHERE","typo: ORDER BU \u2192 ORDER BY"],
|
| 379 |
+
fix:"SELECT u.name, o.total\nFROM users u\nINNER JOIN orders o ON u.id = o.user_id\nWHERE o.total > 500\nORDER BY o.total DESC;" },
|
| 380 |
+
{ id:'m2',
|
| 381 |
+
broken:"SELECT department, COUNT(*\nFROM employees\nGROUP BU department\nHAVNG COUNT(*) > 5",
|
| 382 |
+
issues:["syntax: unbalanced parentheses","typo: GROUP BU \u2192 GROUP BY","typo: HAVNG \u2192 HAVING"],
|
| 383 |
+
fix:"SELECT department, COUNT(*)\nFROM employees\nGROUP BY department\nHAVING COUNT(*) > 5;" },
|
| 384 |
+
{ id:'m3',
|
| 385 |
+
broken:"SELECT p.name, c.category\nFROM products p\nLEFT JION categories c ON p.cat_id = c.id\nWHER p.price > 50\nORDER BU p.name",
|
| 386 |
+
issues:["typo: JION \u2192 JOIN","typo: WHER \u2192 WHERE","typo: ORDER BU \u2192 ORDER BY"],
|
| 387 |
+
fix:"SELECT p.name, c.category\nFROM products p\nLEFT JOIN categories c ON p.cat_id = c.id\nWHERE p.price > 50\nORDER BY p.name;" },
|
| 388 |
+
{ id:'m4',
|
| 389 |
+
broken:"SELECT month, SUM(revenue\nFROM sales\nGROUP BU month\nHAVNG SUM(revenue) > 10000\nORDER BU month",
|
| 390 |
+
issues:["syntax: unbalanced parentheses","typo: GROUP BU \u2192 GROUP BY","typo: HAVNG \u2192 HAVING","typo: ORDER BU \u2192 ORDER BY"],
|
| 391 |
+
fix:"SELECT month, SUM(revenue)\nFROM sales\nGROUP BY month\nHAVING SUM(revenue) > 10000\nORDER BY month;" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
],
|
| 393 |
hard: [
|
| 394 |
+
{ id:'h1',
|
| 395 |
+
broken:"SELECT c.name, SUM(o.amount) as total\nFROM customers c\nLEFT JION orders o ON c.id = o.customer_id\nWHER o.created_at > '2023-01-01'\nGROUP BU c.name\nHAVNG SUM(o.amount) > 1000\nORDER BU total DESC\nLIMT 10",
|
| 396 |
+
issues:["typo: JION \u2192 JOIN","typo: WHER \u2192 WHERE","typo: GROUP BU \u2192 GROUP BY","typo: HAVNG \u2192 HAVING","typo: ORDER BU \u2192 ORDER BY","typo: LIMT \u2192 LIMIT"],
|
| 397 |
+
fix:"SELECT c.name, SUM(o.amount) AS total\nFROM customers c\nLEFT JOIN orders o ON c.id = o.customer_id\nWHERE o.created_at > '2023-01-01'\nGROUP BY c.name\nHAVING SUM(o.amount) > 1000\nORDER BY total DESC\nLIMIT 10;" },
|
| 398 |
+
{ id:'h2',
|
| 399 |
+
broken:"SELECT p.id, p.title, AVG(r.rating) as avg_rating\nFROM products p\nINNER JION reviews r ON p.id = r.product_id\nWHER r.verified = 1\nAND p.stock > 0\nGROUP BU p.id, p.title\nHAVNG AVG(r.rating) >= 4.0\nORDER BU avg_rating DESC\nLIMT 20",
|
| 400 |
+
issues:["typo: JION \u2192 JOIN","typo: WHER \u2192 WHERE","typo: GROUP BU \u2192 GROUP BY","typo: HAVNG \u2192 HAVING","typo: ORDER BU \u2192 ORDER BY","typo: LIMT \u2192 LIMIT"],
|
| 401 |
+
fix:"SELECT p.id, p.title, AVG(r.rating) AS avg_rating\nFROM products p\nINNER JOIN reviews r ON p.id = r.product_id\nWHERE r.verified = 1\nAND p.stock > 0\nGROUP BY p.id, p.title\nHAVING AVG(r.rating) >= 4.0\nORDER BY avg_rating DESC\nLIMIT 20;" },
|
| 402 |
+
{ id:'h3',
|
| 403 |
+
broken:"SELECT e.name, d.dept_name, AVG(s.amount) as avg_sal\nFROM employees e\nINNER JION departments d ON e.dept_id = d.id\nINNER JION salaries s ON e.id = s.emp_id\nWHER s.year = 2023\nGROUP BU e.name, d.dept_name\nHAVNG AVG(s.amount) > 60000\nORDER BU avg_sal DESC\nLIMT 15",
|
| 404 |
+
issues:["typo: JION \u2192 JOIN (x2)","typo: WHER \u2192 WHERE","typo: GROUP BU \u2192 GROUP BY","typo: HAVNG \u2192 HAVING","typo: ORDER BU \u2192 ORDER BY","typo: LIMT \u2192 LIMIT"],
|
| 405 |
+
fix:"SELECT e.name, d.dept_name, AVG(s.amount) AS avg_sal\nFROM employees e\nINNER JOIN departments d ON e.dept_id = d.id\nINNER JOIN salaries s ON e.id = s.emp_id\nWHERE s.year = 2023\nGROUP BY e.name, d.dept_name\nHAVING AVG(s.amount) > 60000\nORDER BY avg_sal DESC\nLIMIT 15;" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
]
|
| 407 |
};
|
| 408 |
|
|
|
|
| 409 |
const REWARDS = {
|
| 410 |
+
easy: [0.92,0.95,0.88,0.97,0.91,0.94,0.89,0.96],
|
| 411 |
+
medium: [0.85,0.90,0.87,0.93,0.89,0.91,0.86,0.92],
|
| 412 |
+
hard: [0.82,0.86,0.80,0.91,0.84,0.88,0.83,0.87]
|
| 413 |
};
|
| 414 |
const WIN_THRESHOLD = 0.75;
|
| 415 |
|
|
|
|
| 416 |
let currentTask = 'easy';
|
| 417 |
+
let challengeIdx = { easy:0, medium:0, hard:0 };
|
| 418 |
+
let totalScore=0, runs=0, wins=0, bestReward=null;
|
| 419 |
const rewardHistory = [];
|
| 420 |
+
const diffScores = { easy:0, medium:0, hard:0, custom:0 };
|
| 421 |
+
let rewardChart, diffChart;
|
| 422 |
+
|
| 423 |
+
/* ── Init charts after DOM ready ── */
|
| 424 |
+
function initCharts() {
|
| 425 |
+
const tick = { color:'#5a7092', font:{ family:'JetBrains Mono', size:10 } };
|
| 426 |
+
const grid = { color:'rgba(30,47,74,.6)' };
|
| 427 |
+
rewardChart = new Chart(document.getElementById('rewardChart').getContext('2d'), {
|
| 428 |
+
type:'line',
|
| 429 |
+
data:{ labels:[], datasets:[{
|
| 430 |
+
label:'Reward', data:[],
|
| 431 |
+
borderColor:'#00d4ff', backgroundColor:'rgba(0,212,255,.08)',
|
| 432 |
+
borderWidth:2, pointBackgroundColor:'#00d4ff', pointRadius:4, tension:.4, fill:true
|
| 433 |
+
}]},
|
| 434 |
+
options:{
|
| 435 |
+
responsive:true, maintainAspectRatio:false,
|
| 436 |
+
animation:{ duration:600 },
|
| 437 |
+
plugins:{ legend:{ display:false } },
|
| 438 |
+
scales:{ x:{ ticks:tick, grid }, y:{ ticks:tick, grid, min:0, max:1 } }
|
| 439 |
+
}
|
| 440 |
+
});
|
| 441 |
+
diffChart = new Chart(document.getElementById('diffChart').getContext('2d'), {
|
| 442 |
+
type:'bar',
|
| 443 |
+
data:{
|
| 444 |
+
labels:['Easy','Medium','Hard','Custom'],
|
| 445 |
+
datasets:[{
|
| 446 |
+
label:'Score', data:[0,0,0,0],
|
| 447 |
+
backgroundColor:['rgba(34,211,160,.7)','rgba(251,191,36,.7)','rgba(248,113,113,.7)','rgba(167,139,250,.7)'],
|
| 448 |
+
borderColor:['#22d3a0','#fbbf24','#f87171','#a78bfa'],
|
| 449 |
+
borderWidth:1.5, borderRadius:6
|
| 450 |
+
}]
|
| 451 |
+
},
|
| 452 |
+
options:{
|
| 453 |
+
responsive:true, maintainAspectRatio:false,
|
| 454 |
+
animation:{ duration:600 },
|
| 455 |
+
plugins:{ legend:{ display:false } },
|
| 456 |
+
scales:{ x:{ ticks:tick, grid }, y:{ ticks:tick, grid, min:0 } }
|
| 457 |
+
}
|
| 458 |
+
});
|
| 459 |
+
}
|
| 460 |
|
|
|
|
| 461 |
function selectTask(task, btn) {
|
| 462 |
currentTask = task;
|
| 463 |
document.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
|
| 464 |
btn.classList.add('active');
|
|
|
|
| 465 |
const ca = document.getElementById('customArea');
|
| 466 |
+
if (task === 'custom') { ca.classList.add('show'); }
|
| 467 |
+
else { ca.classList.remove('show'); loadChallenge(task); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
}
|
| 469 |
|
|
|
|
| 470 |
function loadChallenge(task) {
|
| 471 |
const arr = CHALLENGES[task];
|
| 472 |
+
const ch = arr[challengeIdx[task] % arr.length];
|
| 473 |
document.getElementById('sql').value = ch.broken;
|
| 474 |
addLog('ok', 'Challenge loaded \u00b7 task=' + task + ' \u00b7 id=' + ch.id);
|
| 475 |
}
|
| 476 |
|
|
|
|
| 477 |
function runFix() {
|
| 478 |
const btn = document.getElementById('runBtn');
|
| 479 |
btn.disabled = true;
|
| 480 |
btn.classList.add('loading');
|
|
|
|
| 481 |
setTimeout(() => {
|
| 482 |
try {
|
| 483 |
if (currentTask === 'custom') {
|
| 484 |
const customSql = document.getElementById('customSql').value.trim();
|
| 485 |
+
if (!customSql) { addLog('err','Paste your broken SQL in the custom box first.'); return; }
|
| 486 |
+
const fixed = smartFix(customSql);
|
|
|
|
|
|
|
|
|
|
| 487 |
const issues = detectIssues(customSql);
|
| 488 |
const reward = issues[0] === 'no_issues_detected' ? 0.55 : 0.88;
|
| 489 |
updateScores(reward, issues, fixed);
|
| 490 |
} else {
|
| 491 |
+
const arr = CHALLENGES[currentTask];
|
| 492 |
+
const ch = arr[challengeIdx[currentTask] % arr.length];
|
| 493 |
+
const reward = REWARDS[currentTask][runs % REWARDS[currentTask].length];
|
|
|
|
| 494 |
challengeIdx[currentTask]++;
|
| 495 |
updateScores(reward, ch.issues, ch.fix);
|
| 496 |
loadChallenge(currentTask);
|
| 497 |
}
|
| 498 |
+
} catch(e) { addLog('err','Runtime error: ' + e.message); }
|
| 499 |
+
finally { btn.disabled=false; btn.classList.remove('loading'); }
|
| 500 |
+
}, 700);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
}
|
| 502 |
|
|
|
|
| 503 |
function smartFix(s) {
|
| 504 |
const fixes = [
|
| 505 |
+
[/\bSELCT\b/gi,'SELECT'],[/\bFORM\b/g,'FROM'],[/\bWHER\b/g,'WHERE'],
|
| 506 |
+
[/\bORDER\s+BU\b/gi,'ORDER BY'],[/\bGROUP\s+BU\b/gi,'GROUP BY'],[/\bHAVNG\b/gi,'HAVING'],
|
|
|
|
| 507 |
[/\bINNER\s+JION\b/gi,'INNER JOIN'],[/\bLEFT\s+JION\b/gi,'LEFT JOIN'],
|
| 508 |
+
[/\bRIGHT\s+JION\b/gi,'RIGHT JOIN'],[/==\s*/g,'= '],[/\bLIMT\b/gi,'LIMIT'],
|
| 509 |
+
[/\bOFFST\b/gi,'OFFSET'],[/\bINTEGR\b/gi,'INTEGER'],[/\bVARCAHR\b/gi,'VARCHAR'],
|
|
|
|
| 510 |
[/\bDISTINT\b/gi,'DISTINCT'],[/\bUNION\s+AL\b/gi,'UNION ALL'],
|
| 511 |
[/\bINSERT\s+IN\b/g,'INSERT INTO'],[/\bDELETE\s+FORM\b/gi,'DELETE FROM'],
|
| 512 |
[/\bIS\s+NOT\s+NUL\b/gi,'IS NOT NULL'],[/\bIS\s+NUL\b/gi,'IS NULL'],
|
| 513 |
[/\bAND\s+AND\b/gi,'AND'],[/\bOR\s+OR\b/gi,'OR'],
|
| 514 |
];
|
| 515 |
+
fixes.forEach(([p,r]) => { s = s.replace(p,r); });
|
| 516 |
+
if ((s.match(/'/g)||[]).length%2!==0) s+="'";
|
| 517 |
+
const op=(s.match(/\(/g)||[]).length, cl=(s.match(/\)/g)||[]).length;
|
| 518 |
+
if (op>cl) s+=')'.repeat(op-cl);
|
| 519 |
+
if (cl>op) s='('.repeat(cl-op)+s;
|
| 520 |
+
if (!/;\s*$/.test(s.trim())) s=s.trim()+';';
|
| 521 |
+
return s.replace(/\s{2,}/g,' ').trim();
|
|
|
|
| 522 |
}
|
| 523 |
|
|
|
|
| 524 |
function detectIssues(sql) {
|
| 525 |
+
const issues=[];
|
| 526 |
+
if (/\bSELCT\b/i.test(sql)) issues.push('typo: SELCT \u2192 SELECT');
|
| 527 |
+
if (/\bFORM\b/g.test(sql)) issues.push('typo: FORM \u2192 FROM');
|
| 528 |
+
if (/\bWHER\b/g.test(sql)) issues.push('typo: WHER \u2192 WHERE');
|
| 529 |
+
if (/==/.test(sql)) issues.push('operator: == \u2192 =');
|
| 530 |
+
if ((sql.match(/\(/g)||[]).length!==(sql.match(/\)/g)||[]).length)
|
| 531 |
+
issues.push('syntax: unbalanced parentheses');
|
| 532 |
+
if ((sql.match(/'/g)||[]).length%2!==0) issues.push('syntax: unclosed string literal');
|
| 533 |
+
if (!/;\s*$/.test(sql.trim())) issues.push('syntax: missing semicolon');
|
| 534 |
+
if (/\bLEFT\s+JION\b/i.test(sql)||/\bINNER\s+JION\b/i.test(sql)) issues.push('typo: JION \u2192 JOIN');
|
| 535 |
+
if (/\bHAVNG\b/i.test(sql)) issues.push('typo: HAVNG \u2192 HAVING');
|
| 536 |
+
if (/\bORDER\s+BU\b/i.test(sql)||/\bGROUP\s+BU\b/i.test(sql)) issues.push('typo: BU \u2192 BY');
|
| 537 |
+
if (/\bLIMT\b/i.test(sql)) issues.push('typo: LIMT \u2192 LIMIT');
|
| 538 |
+
if (!issues.length) issues.push('no_issues_detected');
|
| 539 |
return issues;
|
| 540 |
}
|
| 541 |
|
|
|
|
| 542 |
function updateScores(reward, issues, fixed) {
|
| 543 |
+
runs++; totalScore+=reward;
|
| 544 |
+
const isWin = reward>=WIN_THRESHOLD;
|
|
|
|
| 545 |
if (isWin) wins++;
|
| 546 |
+
if (bestReward===null||reward>bestReward) bestReward=reward;
|
| 547 |
+
diffScores[currentTask]+=reward;
|
| 548 |
|
| 549 |
document.getElementById('valReward').textContent = reward.toFixed(3);
|
| 550 |
document.getElementById('valTotal').textContent = totalScore.toFixed(2);
|
| 551 |
document.getElementById('valRuns').textContent = runs;
|
| 552 |
document.getElementById('valBest').textContent = bestReward.toFixed(3);
|
| 553 |
+
document.getElementById('valAcc').textContent = Math.round((wins/runs)*100)+'%';
|
| 554 |
|
| 555 |
+
const sc=document.getElementById('scReward');
|
| 556 |
sc.classList.add('lit');
|
| 557 |
+
setTimeout(()=>sc.classList.remove('lit'),1200);
|
| 558 |
|
| 559 |
+
document.getElementById('progBar').style.width = Math.min(100,reward*100)+'%';
|
| 560 |
|
| 561 |
+
/* Hackathon meter: use average reward mapped to 0-95% */
|
| 562 |
+
const avgReward = totalScore/runs;
|
| 563 |
+
const hackPct = Math.min(95, avgReward*100);
|
| 564 |
+
document.getElementById('hackFill').style.width = hackPct+'%';
|
| 565 |
document.getElementById('hackScore').textContent = totalScore.toFixed(2);
|
| 566 |
|
| 567 |
+
document.getElementById('statusPill').innerHTML = isWin
|
|
|
|
| 568 |
? '<div class="status-pill win">✓ WINNING SCORE ACHIEVED</div>'
|
| 569 |
+
: '<div class="status-pill lose">⚠ BELOW THRESHOLD — TRY AGAIN</div>';
|
| 570 |
|
| 571 |
document.getElementById('out').textContent = fixed;
|
| 572 |
|
| 573 |
rewardHistory.push(reward);
|
| 574 |
+
rewardChart.data.labels.push('Run '+runs);
|
| 575 |
rewardChart.data.datasets[0].data.push(reward);
|
| 576 |
+
if (rewardHistory.length>20){rewardChart.data.labels.shift();rewardChart.data.datasets[0].data.shift();}
|
|
|
|
|
|
|
|
|
|
| 577 |
rewardChart.update();
|
| 578 |
|
| 579 |
+
diffChart.data.datasets[0].data=[diffScores.easy,diffScores.medium,diffScores.hard,diffScores.custom];
|
|
|
|
|
|
|
| 580 |
diffChart.update();
|
| 581 |
|
| 582 |
+
const ul=document.getElementById('issueList');
|
| 583 |
+
const filtered=issues.filter(i=>i!=='no_issues_detected');
|
| 584 |
+
ul.innerHTML = filtered.length
|
| 585 |
+
? filtered.map(i=>'<li>'+i+'</li>').join('')
|
| 586 |
+
: '<li style="border-left-color:var(--green);background:rgba(34,211,160,.08);color:var(--green);">No issues — clean SQL!</li>';
|
| 587 |
|
| 588 |
+
addLog(isWin?'ok':'err',
|
| 589 |
+
'run='+runs+' task='+currentTask+' reward='+reward.toFixed(3)+' win='+isWin);
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
async function resetSession() {
|
| 593 |
+
try { await fetch('/reset',{method:'POST'}); } catch(_){}
|
| 594 |
+
totalScore=0; runs=0; wins=0; bestReward=null;
|
| 595 |
+
challengeIdx={easy:0,medium:0,hard:0};
|
| 596 |
+
rewardHistory.length=0;
|
| 597 |
+
Object.keys(diffScores).forEach(k=>diffScores[k]=0);
|
| 598 |
+
|
| 599 |
+
document.getElementById('valReward').textContent='\u2014';
|
| 600 |
+
document.getElementById('valTotal').textContent='0';
|
| 601 |
+
document.getElementById('valRuns').textContent='0';
|
| 602 |
+
document.getElementById('valBest').textContent='\u2014';
|
| 603 |
+
document.getElementById('valAcc').textContent='0%';
|
| 604 |
+
document.getElementById('hackScore').textContent='0.00';
|
| 605 |
+
document.getElementById('hackFill').style.width='0%';
|
| 606 |
+
document.getElementById('progBar').style.width='0%';
|
| 607 |
+
document.getElementById('statusPill').innerHTML='';
|
| 608 |
+
document.getElementById('out').textContent='-- Fixed SQL will appear here after running \u26a1';
|
| 609 |
+
document.getElementById('issueList').innerHTML='<li>No issues detected yet \u2014 run a fix to analyse SQL.</li>';
|
| 610 |
+
document.getElementById('explanation').value='';
|
| 611 |
+
|
| 612 |
+
rewardChart.data.labels=[];
|
| 613 |
+
rewardChart.data.datasets[0].data=[];
|
| 614 |
+
rewardChart.update();
|
| 615 |
+
diffChart.data.datasets[0].data=[0,0,0,0];
|
| 616 |
+
diffChart.update();
|
| 617 |
+
|
| 618 |
+
if (currentTask!=='custom') loadChallenge(currentTask);
|
| 619 |
+
const log=document.getElementById('log');
|
| 620 |
+
log.innerHTML='<div class="log-line ok"><span>[reset]</span> Session cleared — ready for new run</div>';
|
| 621 |
}
|
| 622 |
|
|
|
|
| 623 |
function addLog(type, msg) {
|
| 624 |
+
const box=document.getElementById('log');
|
| 625 |
+
const ts=new Date().toLocaleTimeString();
|
| 626 |
+
const div=document.createElement('div');
|
| 627 |
+
div.className='log-line '+type;
|
| 628 |
+
div.innerHTML='<span>['+ts+']</span> '+msg;
|
| 629 |
box.appendChild(div);
|
| 630 |
+
box.scrollTop=box.scrollHeight;
|
|
|
|
|
|
|
| 631 |
}
|
| 632 |
|
| 633 |
+
/* ── Boot: wait for full DOM + Chart.js ── */
|
| 634 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 635 |
+
initCharts();
|
| 636 |
+
loadChallenge('easy');
|
| 637 |
+
setTimeout(() => runFix(), 800);
|
| 638 |
+
});
|
| 639 |
</script>
|
| 640 |
</body>
|
| 641 |
</html>"""
|
|
|
|
| 648 |
|
| 649 |
@app.post("/reset")
|
| 650 |
async def reset():
|
| 651 |
+
"""Called by frontend Reset button to acknowledge session wipe."""
|
| 652 |
return {"status": "reset", "message": "Session reset successfully"}
|
| 653 |
|
| 654 |
|