abrar6024 commited on
Commit
c8dc7a5
·
1 Parent(s): 3574a51

updated UI

Browse files
Files changed (1) hide show
  1. 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: #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
- --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;font-size:22px;
48
- box-shadow:var(--glow);flex-shrink:0;
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;border:1.5px solid var(--border);
61
- background:var(--card);cursor:pointer;font-family:'Syne',sans-serif;font-size:13px;font-weight:700;
62
- color:var(--muted);letter-spacing:.8px;transition:all .2s;position:relative;overflow:hidden;
 
 
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{border-color:var(--accent);color:var(--text);transform:translateY(-2px);box-shadow:var(--glow);}
 
 
 
71
  .diff-btn.active{color:var(--accent);}
72
  .diff-btn .pill{
73
- display:inline-block;margin-left:8px;padding:2px 8px;border-radius:8px;font-size:10px;
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;display:flex;align-items:center;gap:8px;
 
 
 
 
 
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;white-space:pre-wrap;
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:var(--glow);}
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 span{color:var(--green);}
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{width:18px;height:18px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:none;margin:0 auto;}
 
 
 
 
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
- 🟢 EASY <span class="pill pill-easy">+100</span>
212
  </button>
213
  <button class="diff-btn" data-task="medium" onclick="selectTask('medium',this)">
214
- 🟡 MEDIUM <span class="pill pill-medium">+200</span>
215
  </button>
216
  <button class="diff-btn" data-task="hard" onclick="selectTask('hard',this)">
217
- 🔴 HARD <span class="pill pill-hard">+400</span>
218
  </button>
219
  <button class="diff-btn" data-task="custom" onclick="selectTask('custom',this)">
220
- 🟣 CUSTOM <span class="pill pill-custom">+&#8734;</span>
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">&#9889; RUN FIX &amp; SCORE</span>
238
  </button>
 
239
  </div>
240
 
241
  <div class="hack-bar">
242
- <div class="hb-title">🏆 Hackathon Score Meter</div>
243
  <div class="hb-row">
244
- <span>Cumulative Score</span><span class="hb-val" id="hackScore">0</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>🥇 Winner</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"></div>
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"></div>
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="Click a difficulty button above to load a challenge..."></textarea>
280
  </div>
281
  <div class="card">
282
- <div class="card-title"><span class="dot" style="background:var(--green);box-shadow:0 0 8px var(--green);"></span>Fixed SQL Output</div>
 
 
 
283
  <div id="statusPill"></div>
284
  <pre id="out">-- Fixed SQL will appear here after running &#9889;</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"><span class="dot" style="background:var(--accent2);box-shadow:0 0 8px var(--accent2);"></span>Reward History</div>
 
 
 
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"><span class="dot" style="background:var(--yellow);box-shadow:0 0 8px var(--yellow);"></span>Difficulty Breakdown</div>
 
 
 
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">Waiting for first run…</div>
305
  </div>
306
  </div>
307
  </div>
308
  </div>
309
 
310
  <div class="card">
311
- <div class="card-title"><span class="dot" style="background:var(--red);box-shadow:0 0 8px var(--red);"></span>Detected Issues</div>
 
 
 
312
  <ul class="issues" id="issueList">
313
- <li>No issues detected yet run a fix to analyse SQL.</li>
314
  </ul>
315
  </div>
316
 
317
  </div>
318
 
319
  <script>
320
- /* ── Challenges library ── */
321
  const CHALLENGES = {
322
  easy: [
323
- {
324
- id: 'e1',
325
- broken: "SELCT name, salary FORM users\n WHER department = 'Engineering'\n ORDER BY salary DESC",
326
- issues: ['typo:SELCT\u2192SELECT','typo:FORM\u2192FROM','typo:WHER\u2192WHERE'],
327
- fix: "SELECT name, salary FROM users\nWHERE department = 'Engineering'\nORDER BY salary DESC;"
328
- },
329
- {
330
- id: 'e2',
331
- broken: "SELECT id, name FORM products\nWHERE price > 100",
332
- issues: ['typo:FORM\u2192FROM','syntax:missing_semicolon'],
333
- fix: "SELECT id, name FROM products\nWHERE price > 100;"
334
- },
335
- {
336
- id: 'e3',
337
- broken: "SELECT * FORM orders WHERE status == 'active'",
338
- issues: ['typo:FORM\u2192FROM','operator:==\u2192='],
339
- fix: "SELECT * FROM orders WHERE status = 'active';"
340
- },
341
- {
342
- id: 'e4',
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
- id: 'm1',
357
- 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",
358
- issues: ['typo:JION\u2192JOIN','typo:WHER\u2192WHERE','typo:ORDER BU\u2192ORDER BY'],
359
- 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;"
360
- },
361
- {
362
- id: 'm2',
363
- broken: "SELECT department, COUNT(*\nFROM employees\nGROUP BU department\nHAVNG COUNT(*) > 5",
364
- issues: ['syntax:unbalanced_parentheses','typo:GROUP BU\u2192GROUP BY','typo:HAVNG\u2192HAVING'],
365
- fix: "SELECT department, COUNT(*)\nFROM employees\nGROUP BY department\nHAVING COUNT(*) > 5;"
366
- },
367
- {
368
- id: 'm3',
369
- 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",
370
- issues: ['typo:JION\u2192JOIN','typo:WHER\u2192WHERE','typo:ORDER BU\u2192ORDER BY'],
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
- id: 'h1',
383
- 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",
384
- issues: ['typo:JION\u2192JOIN','typo:WHER\u2192WHERE','typo:GROUP BU\u2192GROUP BY','typo:HAVNG\u2192HAVING','typo:ORDER BU\u2192ORDER BY','typo:LIMT\u2192LIMIT'],
385
- 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;"
386
- },
387
- {
388
- id: 'h2',
389
- 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",
390
- issues: ['typo:JION\u2192JOIN','typo:WHER\u2192WHERE','typo:GROUP BU\u2192GROUP BY','typo:HAVNG\u2192HAVING','typo:ORDER BU\u2192ORDER BY','typo:LIMT\u2192LIMIT'],
391
- 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;"
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, 0.95, 0.88, 0.97, 0.91, 0.94, 0.89, 0.96],
405
- medium: [0.85, 0.90, 0.87, 0.93, 0.89, 0.91, 0.86, 0.92],
406
- hard: [0.82, 0.86, 0.80, 0.91, 0.84, 0.88, 0.83, 0.87]
407
  };
408
  const WIN_THRESHOLD = 0.75;
409
 
410
- /* ── State ── */
411
  let currentTask = 'easy';
412
- let challengeIdx = { easy: 0, medium: 0, hard: 0 };
413
- let totalScore = 0, runs = 0, wins = 0, bestReward = null;
414
  const rewardHistory = [];
415
- const diffScores = { easy: 0, medium: 0, hard: 0, custom: 0 };
416
-
417
- /* ── Chart setup ── */
418
- const chartDefaults = {
419
- plugins: { legend: { labels: { color: '#5a7092', font: { family: 'JetBrains Mono', size: 10 } } } },
420
- scales: {
421
- x: { ticks: { color: '#5a7092', font: { family: 'JetBrains Mono', size: 10 } }, grid: { color: 'rgba(30,47,74,.6)' } },
422
- y: { ticks: { color: '#5a7092', font: { family: 'JetBrains Mono', size: 10 } }, grid: { color: 'rgba(30,47,74,.6)' } }
423
- }
424
- };
425
-
426
- const rewardChart = new Chart(document.getElementById('rewardChart').getContext('2d'), {
427
- type: 'line',
428
- data: {
429
- labels: [],
430
- datasets: [{
431
- label: 'Reward', data: [],
432
- borderColor: '#00d4ff', backgroundColor: 'rgba(0,212,255,.08)',
433
- borderWidth: 2, pointBackgroundColor: '#00d4ff', pointRadius: 4, tension: .4, fill: true
434
- }]
435
- },
436
- options: { ...chartDefaults, animation: { duration: 600 }, plugins: { legend: { display: false } } }
437
- });
438
-
439
- const diffChart = new Chart(document.getElementById('diffChart').getContext('2d'), {
440
- type: 'bar',
441
- data: {
442
- labels: ['Easy', 'Medium', 'Hard', 'Custom'],
443
- datasets: [{
444
- label: 'Score', data: [0, 0, 0, 0],
445
- backgroundColor: ['rgba(34,211,160,.7)','rgba(251,191,36,.7)','rgba(248,113,113,.7)','rgba(167,139,250,.7)'],
446
- borderColor: ['#22d3a0','#fbbf24','#f87171','#a78bfa'],
447
- borderWidth: 1.5, borderRadius: 6
448
- }]
449
- },
450
- options: { ...chartDefaults, animation: { duration: 600 }, plugins: { legend: { display: false } } }
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' ? ca.classList.add('show') : ca.classList.remove('show');
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 = arr[challengeIdx[task] % arr.length];
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
- addLog('err', 'Paste your broken SQL in the custom box.');
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 = CHALLENGES[currentTask];
496
- const ch = arr[challengeIdx[currentTask] % arr.length];
497
- const rArr = REWARDS[currentTask];
498
- const reward = rArr[runs % rArr.length];
499
  challengeIdx[currentTask]++;
500
  updateScores(reward, ch.issues, ch.fix);
501
  loadChallenge(currentTask);
502
  }
503
- } catch (e) {
504
- addLog('err', 'Error: ' + e.message);
505
- } finally {
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
- [/\bWHER\b/g,'WHERE'],[/\bORDER\s+BU\b/gi,'ORDER BY'],
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
- [/\bLIMT\b/gi,'LIMIT'],[/\bOFFST\b/gi,'OFFSET'],
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, r]) => { s = s.replace(p, r); });
528
- const sq = (s.match(/'/g) || []).length;
529
- if (sq % 2 !== 0) s += "'";
530
- const op = (s.match(/\(/g) || []).length, cl = (s.match(/\)/g) || []).length;
531
- if (op > cl) s += ')'.repeat(op - cl);
532
- if (cl > op) s = '('.repeat(cl - op) + s;
533
- if (!/;\s*$/.test(s.trim())) s = s.trim() + ';';
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)) issues.push('typo:SELCT\u2192SELECT');
541
- if (/\bFORM\b/g.test(sql)) issues.push('typo:FORM\u2192FROM');
542
- if (/\bWHER\b/g.test(sql)) issues.push('typo:WHER\u2192WHERE');
543
- if (/==/.test(sql)) issues.push('operator:==\u2192=');
544
- if ((/\(/g.exec(sql)||[]).length !== (/\)/g.exec(sql)||[]).length) issues.push('syntax:unbalanced_parentheses');
545
- if ((/'/g.exec(sql)||[]).length % 2 !== 0) issues.push('syntax:unclosed_string_literal');
546
- if (!/;\s*$/.test(sql.trim())) issues.push('syntax:missing_semicolon');
547
- if (/\bLEFT\s+JION\b/i.test(sql)||/\bINNER\s+JION\b/i.test(sql)) issues.push('typo:JION\u2192JOIN');
548
- if (/\bHAVNG\b/i.test(sql)) issues.push('typo:HAVNG\u2192HAVING');
549
- if (/\bORDER\s+BU\b/i.test(sql)||/\bGROUP\s+BU\b/i.test(sql)) issues.push('typo:BU\u2192BY');
550
- if (/\bLIMT\b/i.test(sql)) issues.push('typo:LIMT\u2192LIMIT');
551
- if (!issues.length) issues.push('no_issues_detected');
 
552
  return issues;
553
  }
554
 
555
- /* ── Update all UI elements ── */
556
  function updateScores(reward, issues, fixed) {
557
- runs++;
558
- totalScore += reward;
559
- const isWin = reward >= WIN_THRESHOLD;
560
  if (isWin) wins++;
561
- if (bestReward === null || reward > bestReward) bestReward = reward;
562
- diffScores[currentTask] += reward;
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 / runs) * 100) + '%';
569
 
570
- const sc = document.getElementById('scReward');
571
  sc.classList.add('lit');
572
- setTimeout(() => sc.classList.remove('lit'), 1200);
573
 
574
- document.getElementById('progBar').style.width = Math.min(100, (reward / 1) * 100) + '%';
575
 
576
- const hackPct = Math.min(100, (totalScore / Math.max(runs, 1)) * 100);
577
- document.getElementById('hackFill').style.width = hackPct + '%';
 
 
578
  document.getElementById('hackScore').textContent = totalScore.toFixed(2);
579
 
580
- const pill = document.getElementById('statusPill');
581
- pill.innerHTML = isWin
582
  ? '<div class="status-pill win">&#10003; WINNING SCORE ACHIEVED</div>'
583
- : '<div class="status-pill lose">&#9888; BELOW THRESHOLD \u2014 TRY AGAIN</div>';
584
 
585
  document.getElementById('out').textContent = fixed;
586
 
587
  rewardHistory.push(reward);
588
- rewardChart.data.labels.push('Run ' + runs);
589
  rewardChart.data.datasets[0].data.push(reward);
590
- if (rewardHistory.length > 20) {
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 = document.getElementById('issueList');
602
- ul.innerHTML = issues.map(i => '<li>' + i.replace(/:/g, ' \u2192 ') + '</li>').join('');
 
 
 
603
 
604
- addLog(isWin ? 'ok' : 'err',
605
- 'run=' + runs + ' task=' + currentTask + ' reward=' + reward.toFixed(3) + ' win=' + isWin);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  }
607
 
608
- /* ── Log helper ── */
609
  function addLog(type, msg) {
610
- const box = document.getElementById('log');
611
- const ts = new Date().toLocaleTimeString();
612
- const div = document.createElement('div');
613
- div.className = 'log-line ' + type;
614
- div.innerHTML = '<span>[' + ts + ']</span> ' + msg;
615
  box.appendChild(div);
616
- box.scrollTop = box.scrollHeight;
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
- loadChallenge('easy');
623
- setTimeout(() => runFix(), 400);
 
 
 
624
  </script>
625
  </body>
626
  </html>"""
@@ -633,7 +648,7 @@ async def root():
633
 
634
  @app.post("/reset")
635
  async def reset():
636
- """Reset endpoint called by the frontend to clear session state."""
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
+ &#128994; EASY <span class="pill pill-easy">+100</span>
227
  </button>
228
  <button class="diff-btn" data-task="medium" onclick="selectTask('medium',this)">
229
+ &#128993; MEDIUM <span class="pill pill-medium">+200</span>
230
  </button>
231
  <button class="diff-btn" data-task="hard" onclick="selectTask('hard',this)">
232
+ &#128308; HARD <span class="pill pill-hard">+400</span>
233
  </button>
234
  <button class="diff-btn" data-task="custom" onclick="selectTask('custom',this)">
235
+ &#128995; CUSTOM <span class="pill pill-custom">+&#8734;</span>
236
  </button>
237
  </div>
238
 
 
251
  <div class="spinner" id="spinner"></div>
252
  <span class="btn-label">&#9889; RUN FIX &amp; SCORE</span>
253
  </button>
254
+ <button class="reset-btn" onclick="resetSession()">&#8635; RESET</button>
255
  </div>
256
 
257
  <div class="hack-bar">
258
+ <div class="hb-title">&#127942; 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>&#127945; 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">&#8212;</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">&#8212;</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 &#9889;</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">&#128994; Ready &mdash; select difficulty and click RUN FIX &amp; 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 &mdash; 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">&#10003; WINNING SCORE ACHIEVED</div>'
569
+ : '<div class="status-pill lose">&#9888; BELOW THRESHOLD &mdash; 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 &mdash; 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 &mdash; 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