abrar6024 commited on
Commit
1f558e0
Β·
1 Parent(s): 76b843b

updated UI

Browse files
Files changed (1) hide show
  1. server/app.py +255 -343
server/app.py CHANGED
@@ -1,80 +1,9 @@
1
- """
2
- SQL Debugger & Optimizer β€” FastAPI Server for HF Spaces
3
- """
4
- from __future__ import annotations
5
- import os
6
- import uuid
7
- from typing import Any, Dict, Optional
8
-
9
- from fastapi import FastAPI, HTTPException, Body
10
  from fastapi.responses import HTMLResponse
11
- from pydantic import BaseModel
12
 
13
- from sql_debugger_env import Action, SQLDebuggerEnv
14
 
15
- app = FastAPI(
16
- title="SQL Debugger & Optimizer β€” OpenEnv",
17
- description="Real-world SQL debugging environment for RL agent evaluation.",
18
- version="1.0.0",
19
- )
20
-
21
- _sessions: Dict[str, SQLDebuggerEnv] = {}
22
-
23
-
24
- class ResetRequest(BaseModel):
25
- task: str = "easy"
26
- session_id: Optional[str] = None
27
-
28
-
29
- class StepRequest(BaseModel):
30
- session_id: str
31
- action: Dict[str, Any]
32
-
33
-
34
- # βœ… HEALTH CHECK
35
- @app.get("/health")
36
- def health():
37
- return {"status": "ok"}
38
-
39
-
40
- # βœ… RESET
41
- @app.post("/reset")
42
- def reset(req: Optional[ResetRequest] = Body(default=None)):
43
- task = req.task if req else "easy"
44
- session_id = req.session_id if req else str(uuid.uuid4())
45
-
46
- env = SQLDebuggerEnv(task=task)
47
- _sessions[session_id] = env
48
- obs = env.reset()
49
-
50
- return {
51
- "session_id": session_id,
52
- "observation": obs.model_dump()
53
- }
54
-
55
-
56
- # βœ… STEP
57
- @app.post("/step")
58
- def step(req: StepRequest):
59
- env = _sessions.get(req.session_id)
60
- if not env:
61
- raise HTTPException(status_code=404, detail="Session not found.")
62
-
63
- action = Action(**req.action)
64
- obs, reward, done, info = env.step(action)
65
-
66
- return {
67
- "observation": obs.model_dump(),
68
- "reward": reward,
69
- "done": done,
70
- "info": info
71
- }
72
-
73
-
74
- # βœ… ROOT UI
75
- @app.get("/", response_class=HTMLResponse)
76
- def ui():
77
- return r"""<!DOCTYPE html>
78
  <html lang="en">
79
  <head>
80
  <meta charset="UTF-8"/>
@@ -98,9 +27,8 @@ def ui():
98
  --glow: 0 0 24px rgba(0,212,255,.25);
99
  }
100
  *{box-sizing:border-box;margin:0;padding:0;}
101
- html,body{height:100%;background:var(--bg);color:var(--text);font-family:'JetBrains Mono',monospace;}
102
 
103
- /* ── Background grid ── */
104
  body::before{
105
  content:'';position:fixed;inset:0;z-index:0;
106
  background-image:
@@ -112,23 +40,21 @@ def ui():
112
 
113
  .wrapper{position:relative;z-index:1;max-width:1200px;margin:0 auto;padding:32px 24px 60px;}
114
 
115
- /* ── Header ── */
116
  header{display:flex;align-items:center;gap:16px;margin-bottom:36px;}
117
  .logo-box{
118
  width:52px;height:52px;border-radius:14px;
119
  background:linear-gradient(135deg,var(--accent2),var(--accent));
120
  display:flex;align-items:center;justify-content:center;font-size:22px;
121
- box-shadow:var(--glow);
122
  }
123
  header h1{font-family:'Syne',sans-serif;font-size:26px;font-weight:800;letter-spacing:-.5px;}
124
  header h1 span{color:var(--accent);}
125
  .badge{
126
  margin-left:auto;padding:5px 14px;border-radius:20px;font-size:11px;font-weight:700;
127
  background:rgba(0,212,255,.1);border:1px solid rgba(0,212,255,.3);color:var(--accent);
128
- letter-spacing:1.5px;text-transform:uppercase;
129
  }
130
 
131
- /* ── Difficulty row ── */
132
  .diff-row{display:flex;gap:12px;margin-bottom:28px;flex-wrap:wrap;}
133
  .diff-btn{
134
  flex:1;min-width:120px;padding:13px 0;border-radius:12px;border:1.5px solid var(--border);
@@ -152,11 +78,9 @@ def ui():
152
  .pill-hard{background:rgba(248,113,113,.15);color:var(--red);}
153
  .pill-custom{background:rgba(124,58,237,.2);color:#a78bfa;}
154
 
155
- /* ── Main grid ── */
156
  .grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;}
157
  @media(max-width:768px){.grid{grid-template-columns:1fr;}}
158
 
159
- /* ── Card ── */
160
  .card{
161
  background:var(--card);border:1.5px solid var(--border);border-radius:16px;
162
  padding:20px;
@@ -165,9 +89,8 @@ def ui():
165
  font-family:'Syne',sans-serif;font-size:11px;font-weight:800;letter-spacing:2px;
166
  text-transform:uppercase;color:var(--muted);margin-bottom:14px;display:flex;align-items:center;gap:8px;
167
  }
168
- .card-title .dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);}
169
 
170
- /* ── Textarea / pre ── */
171
  textarea{
172
  width:100%;height:150px;background:#060d1a;border:1.5px solid var(--border);
173
  border-radius:10px;color:var(--text);font-family:'JetBrains Mono',monospace;
@@ -181,13 +104,11 @@ def ui():
181
  word-break:break-all;color:var(--green);overflow:auto;
182
  }
183
 
184
- /* ── Custom SQL ── */
185
  .custom-area{display:none;margin-bottom:20px;}
186
  .custom-area.show{display:block;}
187
  .custom-area textarea{height:80px;}
188
- .custom-label{font-size:11px;color:var(--muted);margin-bottom:6px;letter-spacing:1px;}
189
 
190
- /* ── Run button ── */
191
  .run-row{display:flex;gap:12px;margin-bottom:20px;align-items:center;}
192
  .run-btn{
193
  flex:1;padding:14px;border-radius:12px;border:none;cursor:pointer;
@@ -197,11 +118,9 @@ def ui():
197
  }
198
  .run-btn:hover{transform:translateY(-2px);box-shadow:0 8px 32px rgba(0,212,255,.35);}
199
  .run-btn:active{transform:translateY(0);}
 
200
 
201
- /* ── Score strip ── */
202
- .score-strip{
203
- display:flex;gap:14px;margin-bottom:20px;flex-wrap:wrap;
204
- }
205
  .score-card{
206
  flex:1;min-width:110px;background:var(--card);border:1.5px solid var(--border);
207
  border-radius:14px;padding:16px 14px;text-align:center;transition:all .3s;
@@ -210,7 +129,6 @@ def ui():
210
  .score-card .sc-val{font-family:'Syne',sans-serif;font-size:28px;font-weight:800;color:var(--accent);}
211
  .score-card .sc-lbl{font-size:10px;color:var(--muted);margin-top:4px;letter-spacing:1.5px;text-transform:uppercase;}
212
 
213
- /* ── Progress bar ── */
214
  .prog-wrap{background:var(--surface);border-radius:99px;height:10px;overflow:hidden;margin-top:8px;}
215
  .prog-bar{
216
  height:100%;border-radius:99px;width:0%;
@@ -219,7 +137,6 @@ def ui():
219
  box-shadow:0 0 12px var(--accent);
220
  }
221
 
222
- /* ── Status badge ── */
223
  .status-pill{
224
  display:inline-flex;align-items:center;gap:6px;padding:6px 14px;
225
  border-radius:20px;font-size:12px;font-weight:700;letter-spacing:.5px;
@@ -227,19 +144,15 @@ def ui():
227
  }
228
  .status-pill.win{background:rgba(34,211,160,.15);border:1px solid var(--green);color:var(--green);}
229
  .status-pill.lose{background:rgba(248,113,113,.12);border:1px solid var(--red);color:var(--red);}
230
- .status-pill.neutral{background:rgba(91,112,146,.12);border:1px solid var(--muted);color:var(--muted);}
231
 
232
- /* ── Chart ── */
233
  .chart-wrap{position:relative;height:220px;}
234
 
235
- /* ── Issues list ── */
236
  .issues{list-style:none;}
237
  .issues li{
238
  padding:8px 12px;border-radius:8px;margin-bottom:6px;font-size:12px;
239
  background:rgba(248,113,113,.08);border-left:3px solid var(--red);color:#fca5a5;
240
  }
241
 
242
- /* ── Log ── */
243
  .log-wrap{
244
  background:#060d1a;border:1.5px solid var(--border);border-radius:12px;
245
  max-height:180px;overflow-y:auto;padding:12px;
@@ -249,7 +162,6 @@ def ui():
249
  .log-line.ok span{color:var(--green);}
250
  .log-line.err span{color:var(--red);}
251
 
252
- /* ── Explanation ── */
253
  .explain-input{
254
  width:100%;padding:10px 14px;border-radius:10px;border:1.5px solid var(--border);
255
  background:#060d1a;color:var(--text);font-family:'JetBrains Mono',monospace;font-size:12px;
@@ -257,7 +169,6 @@ def ui():
257
  }
258
  .explain-input:focus{border-color:var(--accent);}
259
 
260
- /* ── Hackathon meter ── */
261
  .hack-bar{
262
  background:var(--card);border:1.5px solid var(--border);border-radius:16px;
263
  padding:20px;margin-bottom:20px;
@@ -274,13 +185,11 @@ def ui():
274
  }
275
  .hack-labels{display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:var(--muted);}
276
 
277
- /* ── Animations ── */
278
  @keyframes fadeUp{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:translateY(0);}}
279
  .card,.score-card,.hack-bar{animation:fadeUp .4s ease both;}
280
 
281
- /* ── Spinner ── */
282
  @keyframes spin{to{transform:rotate(360deg);}}
283
- .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;}
284
  .loading .spinner{display:block;}
285
  .loading .btn-label{display:none;}
286
  </style>
@@ -288,17 +197,15 @@ def ui():
288
  <body>
289
  <div class="wrapper">
290
 
291
- <!-- Header -->
292
  <header>
293
- <div class="logo-box">⚑</div>
294
  <div>
295
- <h1>SQL <span>Debugger</span> & Optimizer</h1>
296
- <div style="font-size:11px;color:var(--muted);margin-top:2px;">RL Environment Β· OpenEnv Protocol</div>
297
  </div>
298
  <div class="badge">v1.0.0</div>
299
  </header>
300
 
301
- <!-- Difficulty Buttons -->
302
  <div class="diff-row">
303
  <button class="diff-btn active" data-task="easy" onclick="selectTask('easy',this)">
304
  🟒 EASY <span class="pill pill-easy">+100</span>
@@ -310,31 +217,27 @@ def ui():
310
  πŸ”΄ HARD <span class="pill pill-hard">+400</span>
311
  </button>
312
  <button class="diff-btn" data-task="custom" onclick="selectTask('custom',this)">
313
- 🟣 CUSTOM <span class="pill pill-custom">+∞</span>
314
  </button>
315
  </div>
316
 
317
- <!-- Custom SQL entry -->
318
  <div class="custom-area" id="customArea">
319
- <div class="custom-label">PASTE YOUR BROKEN SQL BELOW</div>
320
  <textarea id="customSql" placeholder="-- Paste broken SQL here for custom challenge..."></textarea>
321
  </div>
322
 
323
- <!-- Explanation -->
324
  <div style="margin-bottom:16px;">
325
- <div class="custom-label" style="margin-bottom:6px;">FIX EXPLANATION (boosts score)</div>
326
  <input class="explain-input" id="explanation" placeholder="e.g. Fixed typo in FROM clause, added missing JOIN condition..."/>
327
  </div>
328
 
329
- <!-- Run -->
330
  <div class="run-row">
331
  <button class="run-btn" id="runBtn" onclick="runFix()">
332
  <div class="spinner" id="spinner"></div>
333
- <span class="btn-label">⚑ RUN FIX &amp; SCORE</span>
334
  </button>
335
  </div>
336
 
337
- <!-- Hackathon Meter -->
338
  <div class="hack-bar">
339
  <div class="hb-title">πŸ† Hackathon Score Meter</div>
340
  <div class="hb-row">
@@ -346,7 +249,6 @@ def ui():
346
  </div>
347
  </div>
348
 
349
- <!-- Score Strip -->
350
  <div class="score-strip">
351
  <div class="score-card" id="scReward">
352
  <div class="sc-val" id="valReward">β€”</div>
@@ -370,37 +272,34 @@ def ui():
370
  </div>
371
  </div>
372
 
373
- <!-- Main Grid -->
374
  <div class="grid">
375
- <!-- Left: SQL panels -->
376
  <div>
377
  <div class="card" style="margin-bottom:16px;">
378
- <div class="card-title"><span class="dot"></span>BROKEN SQL (from challenge)</div>
379
  <textarea id="sql" placeholder="Click a difficulty button above to load a challenge..."></textarea>
380
  </div>
381
  <div class="card">
382
- <div class="card-title"><span class="dot" style="background:var(--green);box-shadow:0 0 8px var(--green);"></span>FIXED SQL OUTPUT</div>
383
  <div id="statusPill"></div>
384
- <pre id="out">-- Fixed SQL will appear here after running ⚑</pre>
385
  <div style="margin-top:12px;">
386
- <div class="custom-label" style="margin-bottom:4px;">SCORE PROGRESS</div>
387
  <div class="prog-wrap"><div class="prog-bar" id="progBar"></div></div>
388
  </div>
389
  </div>
390
  </div>
391
 
392
- <!-- Right: Charts & logs -->
393
  <div>
394
  <div class="card" style="margin-bottom:16px;">
395
- <div class="card-title"><span class="dot" style="background:var(--accent2);box-shadow:0 0 8px var(--accent2);"></span>REWARD HISTORY</div>
396
  <div class="chart-wrap"><canvas id="rewardChart"></canvas></div>
397
  </div>
398
  <div class="card" style="margin-bottom:16px;">
399
- <div class="card-title"><span class="dot" style="background:var(--yellow);box-shadow:0 0 8px var(--yellow);"></span>DIFFICULTY BREAKDOWN</div>
400
  <div class="chart-wrap"><canvas id="diffChart"></canvas></div>
401
  </div>
402
  <div class="card">
403
- <div class="card-title"><span class="dot"></span>SESSION LOG</div>
404
  <div class="log-wrap" id="log">
405
  <div class="log-line">Waiting for first run…</div>
406
  </div>
@@ -408,9 +307,8 @@ def ui():
408
  </div>
409
  </div>
410
 
411
- <!-- Issues -->
412
  <div class="card">
413
- <div class="card-title"><span class="dot" style="background:var(--red);box-shadow:0 0 8px var(--red);"></span>DETECTED ISSUES</div>
414
  <ul class="issues" id="issueList">
415
  <li>No issues detected yet β€” run a fix to analyse SQL.</li>
416
  </ul>
@@ -419,306 +317,320 @@ def ui():
419
  </div>
420
 
421
  <script>
422
- /* ─── State ─── */
423
- let sid = null, challenge = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  let currentTask = 'easy';
 
425
  let totalScore = 0, runs = 0, wins = 0, bestReward = null;
426
  const rewardHistory = [];
427
- const diffScores = { easy:0, medium:0, hard:0, custom:0 };
428
 
429
- const THRESHOLDS = { easy:0.6, medium:0.6, hard:0.6, custom:0.5 };
430
-
431
- /* ─── Charts ─── */
432
  const chartDefaults = {
433
- color: '#e2eaf8',
434
- plugins:{ legend:{ labels:{ color:'#5a7092', font:{ family:'JetBrains Mono', size:10 } } } },
435
- scales:{
436
- x:{ ticks:{ color:'#5a7092', font:{ family:'JetBrains Mono', size:10 } }, grid:{ color:'rgba(30,47,74,.6)' } },
437
- y:{ ticks:{ color:'#5a7092', font:{ family:'JetBrains Mono', size:10 } }, grid:{ color:'rgba(30,47,74,.6)' } }
438
  }
439
  };
440
 
441
- const rewardCtx = document.getElementById('rewardChart').getContext('2d');
442
- const rewardChart = new Chart(rewardCtx, {
443
  type: 'line',
444
- data:{
445
- labels:[],
446
- datasets:[{
447
- label:'Reward',
448
- data:[],
449
- borderColor:'#00d4ff',
450
- backgroundColor:'rgba(0,212,255,.08)',
451
- borderWidth:2,
452
- pointBackgroundColor:'#00d4ff',
453
- pointRadius:4,
454
- tension:.4,
455
- fill:true
456
  }]
457
  },
458
- options:{ ...chartDefaults, animation:{ duration:600 }, plugins:{ legend:{ display:false } } }
459
  });
460
 
461
- const diffCtx = document.getElementById('diffChart').getContext('2d');
462
- const diffChart = new Chart(diffCtx, {
463
- type:'bar',
464
- data:{
465
- labels:['Easy','Medium','Hard','Custom'],
466
- datasets:[{
467
- label:'Score',
468
- data:[0,0,0,0],
469
- backgroundColor:['rgba(34,211,160,.7)','rgba(251,191,36,.7)','rgba(248,113,113,.7)','rgba(167,139,250,.7)'],
470
- borderColor:['#22d3a0','#fbbf24','#f87171','#a78bfa'],
471
- borderWidth:1.5,
472
- borderRadius:6
473
  }]
474
  },
475
- options:{ ...chartDefaults, animation:{ duration:600 }, plugins:{ legend:{ display:false } } }
476
  });
477
 
478
- /* ─── Select task ─── */
479
  function selectTask(task, btn) {
480
  currentTask = task;
481
- document.querySelectorAll('.diff-btn').forEach(b=>b.classList.remove('active'));
482
  btn.classList.add('active');
483
 
484
  const ca = document.getElementById('customArea');
485
- if(task==='custom') { ca.classList.add('show'); }
486
- else { ca.classList.remove('show'); }
487
 
488
- if(task !== 'custom') loadChallenge(task);
 
 
 
489
  }
490
 
491
- /* ─── Load challenge ─── */
492
- async function loadChallenge(task) {
493
- try {
494
- const r = await fetch('/reset',{
495
- method:'POST',
496
- headers:{'Content-Type':'application/json'},
497
- body: JSON.stringify({ task })
498
- });
499
- const d = await r.json();
500
- sid = d.session_id;
501
- challenge = d.observation.challenge;
502
- document.getElementById('sql').value = challenge.broken_sql || '-- No SQL provided';
503
- addLog('ok', `Challenge loaded Β· task=${task} Β· id=${challenge.id}`);
504
- } catch(e) {
505
- addLog('err', `Failed to load challenge: ${e.message}`);
506
- }
507
  }
508
 
509
- /* ─── Run Fix ─── */
510
- async function runFix() {
511
  const btn = document.getElementById('runBtn');
 
512
  btn.classList.add('loading');
513
 
514
- try {
515
- let brokenSql = document.getElementById('sql').value.trim();
516
- let customSql = document.getElementById('customSql').value.trim();
517
- const explanation = document.getElementById('explanation').value.trim() || 'auto fix';
518
-
519
- // For custom, auto-load a session first
520
- if(currentTask === 'custom') {
521
- if(!customSql) { addLog('err','Paste your broken SQL in the custom box.'); btn.classList.remove('loading'); return; }
522
- // Load a session for custom (use easy as base env)
523
- const rr = await fetch('/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({task:'easy'})});
524
- const rd = await rr.json();
525
- sid = rd.session_id;
526
- challenge = rd.observation.challenge;
527
- brokenSql = customSql;
528
- challenge.broken_sql = customSql;
529
- }
530
-
531
- if(!sid || !challenge) { addLog('err','Click a difficulty button first.'); btn.classList.remove('loading'); return; }
532
-
533
- /* ── Smart fixer ── */
534
- const fixed = smartFix(brokenSql);
535
- const issues = detectIssues(brokenSql);
536
-
537
- const resp = await fetch('/step',{
538
- method:'POST',
539
- headers:{'Content-Type':'application/json'},
540
- body: JSON.stringify({
541
- session_id: sid,
542
- action:{
543
- challenge_id: challenge.id,
544
- fixed_sql: fixed,
545
- explanation: explanation,
546
- detected_issues: issues
547
  }
548
- })
549
- });
550
- const data = await resp.json();
551
- const reward = data.reward ?? 0;
552
-
553
- /* ── Update UI ── */
554
- updateScores(reward, issues, fixed, data);
555
-
556
- /* ── Reload next challenge ── */
557
- if(currentTask !== 'custom') loadChallenge(currentTask);
558
-
559
- } catch(e) {
560
- addLog('err', `Error: ${e.message}`);
561
- } finally {
562
- btn.classList.remove('loading');
563
- }
 
 
 
 
564
  }
565
 
566
- /* ─── Smart SQL fixer (high-reward logic) ─── */
567
- function smartFix(sql) {
568
- let s = sql;
569
-
570
- // Keyword typos
571
  const fixes = [
572
- [/\bSELCT\b/gi,'SELECT'],[/\bSELECT\b/gi,'SELECT'],
573
- [/\bFORM\b/g,'FROM'],[/\bFROM\b/gi,'FROM'],
574
- [/\bWHER\b/g,'WHERE'],[/\bWHERE\b/gi,'WHERE'],
575
- [/\bORDER\s+BU\b/gi,'ORDER BY'],[/\bGROUP\s+BU\b/gi,'GROUP BY'],
576
- [/\bHAVNG\b/gi,'HAVING'],[/\bHAVING\b/gi,'HAVING'],
577
  [/\bINNER\s+JION\b/gi,'INNER JOIN'],[/\bLEFT\s+JION\b/gi,'LEFT JOIN'],
578
- [/\bJOIN\s+ON\b/gi,'JOIN'],[/\bINSERT\s+IN\b/g,'INSERT INTO'],
579
- [/\bDELETE\s+FORM\b/gi,'DELETE FROM'],
580
- [/\bUPDATE\b/gi,'UPDATE'],[/\bSET\b/gi,'SET'],
581
- [/\bDISTINT\b/gi,'DISTINCT'],[/\bCOUNT\s*\(\s*\)/gi,'COUNT(*)'],
582
- [/\bNULL\b/gi,'NULL'],[/\bIS\s+NOT\s+NUL\b/gi,'IS NOT NULL'],
583
- [/\bIS\s+NUL\b/gi,'IS NULL'],
584
  [/\bLIMT\b/gi,'LIMIT'],[/\bOFFST\b/gi,'OFFSET'],
585
- [/\bUNION\s+AL\b/gi,'UNION ALL'],
586
- [/\bCREATE\s+TABL\b/gi,'CREATE TABLE'],
587
- [/\bALTER\s+TABL\b/gi,'ALTER TABLE'],
588
- [/\bDROP\s+TABL\b/gi,'DROP TABLE'],
589
- [/\bVARCAHR\b/gi,'VARCHAR'],[/\bINTEGR\b/gi,'INTEGER'],
590
- [/==\s*/g,'= '],[/\bAND\s+AND\b/gi,'AND'],
591
- [/\bOR\s+OR\b/gi,'OR'],[/\bNOT\s+NOT\b/gi,'NOT'],
592
- [/\bTRUNCATE\b/gi,'TRUNCATE'],[/\bTRANSACTION\b/gi,'TRANSACTION'],
593
  ];
594
-
595
- fixes.forEach(([pat,rep])=>{ s = s.replace(pat,rep); });
596
-
597
- // Unclosed quotes fix
598
- const sq = (s.match(/'/g)||[]).length;
599
- if(sq%2!==0) s += "'";
600
-
601
- // Unclosed parens fix
602
- const op=(s.match(/\(/g)||[]).length, cl=(s.match(/\)/g)||[]).length;
603
- if(op>cl) s += ')'.repeat(op-cl);
604
- if(cl>op) s = '('.repeat(cl-op) + s;
605
-
606
- // Missing semicolon
607
- if(!/;\s*$/.test(s.trim())) s = s.trim() + ';';
608
-
609
- // Normalise whitespace
610
- s = s.replace(/\s{2,}/g,' ').trim();
611
-
612
- return s;
613
  }
614
 
615
- /* ─── Detect issues ─── */
616
  function detectIssues(sql) {
617
  const issues = [];
618
- if(/\bSELCT\b/i.test(sql)) issues.push('typo:SELCT→SELECT');
619
- if(/\bFORM\b/g.test(sql)) issues.push('typo:FORM→FROM');
620
- if(/\bWHER\b/g.test(sql)) issues.push('typo:WHER→WHERE');
621
- if(/==/.test(sql)) issues.push('operator:==β†’=');
622
- if((/\(/g.exec(sql)||[]).length !== (/\)/g.exec(sql)||[]).length) issues.push('syntax:unbalanced_parentheses');
623
- if((/'/g.exec(sql)||[]).length%2!==0) issues.push('syntax:unclosed_string_literal');
624
- if(!/;\s*$/.test(sql.trim())) issues.push('syntax:missing_semicolon');
625
- if(/\bLEFT\s+JION\b/i.test(sql)||/\bINNER\s+JION\b/i.test(sql)) issues.push('typo:JION→JOIN');
626
- if(/\bHAVNG\b/i.test(sql)) issues.push('typo:HAVNG→HAVING');
627
- if(/\bINTEGR\b/i.test(sql)) issues.push('typo:INTEGR→INTEGER');
628
- if(!issues.length) issues.push('no_issues_detected');
 
629
  return issues;
630
  }
631
 
632
- /* ─── Update all UI ─── */
633
- function updateScores(reward, issues, fixed, data) {
634
  runs++;
635
  totalScore += reward;
636
- const isWin = reward >= (THRESHOLDS[currentTask] || 0.6);
637
- if(isWin) wins++;
638
- if(bestReward===null || reward > bestReward) bestReward = reward;
639
-
640
  diffScores[currentTask] += reward;
641
 
642
- /* Score cards */
643
- document.getElementById('valReward').textContent = reward.toFixed ? reward.toFixed(3) : reward;
644
- document.getElementById('valTotal').textContent = totalScore.toFixed(2);
645
- document.getElementById('valRuns').textContent = runs;
646
- document.getElementById('valBest').textContent = bestReward.toFixed ? bestReward.toFixed(3) : bestReward;
647
- document.getElementById('valAcc').textContent = Math.round((wins/runs)*100)+'%';
648
 
649
- document.getElementById('scReward').classList.add('lit');
650
- setTimeout(()=>document.getElementById('scReward').classList.remove('lit'),1200);
 
651
 
652
- /* Progress bar */
653
- const pct = Math.min(100, (reward/(THRESHOLDS[currentTask]||1))*100);
654
- document.getElementById('progBar').style.width = pct+'%';
655
 
656
- /* Hackathon meter */
657
- const hackPct = Math.min(100, (totalScore / (runs * 1)) * 100);
658
- document.getElementById('hackFill').style.width = hackPct+'%';
659
  document.getElementById('hackScore').textContent = totalScore.toFixed(2);
660
 
661
- /* Status pill */
662
  const pill = document.getElementById('statusPill');
663
- if(isWin){
664
- pill.innerHTML = '<div class="status-pill win">βœ… WINNING SCORE ACHIEVED</div>';
665
- } else {
666
- pill.innerHTML = '<div class="status-pill lose">⚠ BELOW THRESHOLD β€” TRY AGAIN</div>';
667
- }
668
 
669
- /* Output */
670
  document.getElementById('out').textContent = fixed;
671
 
672
- /* Reward chart */
673
  rewardHistory.push(reward);
674
- rewardChart.data.labels.push(`Run ${runs}`);
675
  rewardChart.data.datasets[0].data.push(reward);
676
- if(rewardHistory.length > 20) {
677
  rewardChart.data.labels.shift();
678
  rewardChart.data.datasets[0].data.shift();
679
  }
680
  rewardChart.update();
681
 
682
- /* Diff chart */
683
  diffChart.data.datasets[0].data = [
684
  diffScores.easy, diffScores.medium, diffScores.hard, diffScores.custom
685
  ];
686
  diffChart.update();
687
 
688
- /* Issues list */
689
  const ul = document.getElementById('issueList');
690
- ul.innerHTML = issues.map(i=>`<li>${i.replace(/:/g,' β†’ ')}</li>`).join('');
691
 
692
- /* Log */
693
- addLog(isWin?'ok':'err',
694
- `run=${runs} task=${currentTask} reward=${typeof reward==='number'?reward.toFixed(3):reward} done=${data.done||false}`);
695
  }
696
 
697
- /* ─── Log helper ─── */
698
  function addLog(type, msg) {
699
  const box = document.getElementById('log');
700
- const ts = new Date().toLocaleTimeString();
701
  const div = document.createElement('div');
702
- div.className = `log-line ${type}`;
703
- div.innerHTML = `<span>[${ts}]</span> ${msg}`;
704
  box.appendChild(div);
705
  box.scrollTop = box.scrollHeight;
706
- // Clear placeholder
707
- const first = box.querySelector('.log-line:not(.ok):not(.err)');
708
- if(first && box.children.length > 1) first.remove();
709
  }
710
 
711
- /* ─── Initial load ─── */
712
  loadChallenge('easy');
 
713
  </script>
714
  </body>
715
  </html>"""
716
 
717
 
718
- def main():
719
- import uvicorn
720
- uvicorn.run("server.app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
721
 
722
 
723
- if __name__ == "__main__":
724
- main()
 
 
1
+ from fastapi import FastAPI
 
 
 
 
 
 
 
 
2
  from fastapi.responses import HTMLResponse
 
3
 
4
+ app = FastAPI()
5
 
6
+ HTML_CONTENT = r"""<!DOCTYPE html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  <html lang="en">
8
  <head>
9
  <meta charset="UTF-8"/>
 
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:
 
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);}
52
  .badge{
53
  margin-left:auto;padding:5px 14px;border-radius:20px;font-size:11px;font-weight:700;
54
  background:rgba(0,212,255,.1);border:1px solid rgba(0,212,255,.3);color:var(--accent);
55
+ letter-spacing:1.5px;text-transform:uppercase;white-space:nowrap;
56
  }
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);
 
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;
 
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;
 
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{
114
  flex:1;padding:14px;border-radius:12px;border:none;cursor:pointer;
 
118
  }
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;
 
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
 
 
132
  .prog-wrap{background:var(--surface);border-radius:99px;height:10px;overflow:hidden;margin-top:8px;}
133
  .prog-bar{
134
  height:100%;border-radius:99px;width:0%;
 
137
  box-shadow:0 0 12px var(--accent);
138
  }
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;
 
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;}
151
  .issues li{
152
  padding:8px 12px;border-radius:8px;margin-bottom:6px;font-size:12px;
153
  background:rgba(248,113,113,.08);border-left:3px solid var(--red);color:#fca5a5;
154
  }
155
 
 
156
  .log-wrap{
157
  background:#060d1a;border:1.5px solid var(--border);border-radius:12px;
158
  max-height:180px;overflow-y:auto;padding:12px;
 
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;
 
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;
 
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>
 
197
  <body>
198
  <div class="wrapper">
199
 
 
200
  <header>
201
+ <div class="logo-box">&#9889;</div>
202
  <div>
203
+ <h1>SQL <span>Debugger</span> &amp; Optimizer</h1>
204
+ <div style="font-size:11px;color:var(--muted);margin-top:2px;">RL Environment &middot; OpenEnv Protocol</div>
205
  </div>
206
  <div class="badge">v1.0.0</div>
207
  </header>
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>
 
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
 
 
224
  <div class="custom-area" id="customArea">
225
+ <div class="custom-label">Paste your broken SQL below</div>
226
  <textarea id="customSql" placeholder="-- Paste broken SQL here for custom challenge..."></textarea>
227
  </div>
228
 
 
229
  <div style="margin-bottom:16px;">
230
+ <div class="custom-label" style="margin-bottom:6px;">Fix Explanation (boosts score)</div>
231
  <input class="explain-input" id="explanation" placeholder="e.g. Fixed typo in FROM clause, added missing JOIN condition..."/>
232
  </div>
233
 
 
234
  <div class="run-row">
235
  <button class="run-btn" id="runBtn" onclick="runFix()">
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">
 
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>
 
272
  </div>
273
  </div>
274
 
 
275
  <div class="grid">
 
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;">
286
+ <div class="custom-label" style="margin-bottom:4px;">Score Progress</div>
287
  <div class="prog-wrap"><div class="prog-bar" id="progBar"></div></div>
288
  </div>
289
  </div>
290
  </div>
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>
 
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>
 
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>"""
627
 
628
 
629
+ @app.get("/", response_class=HTMLResponse)
630
+ async def root():
631
+ return HTMLResponse(content=HTML_CONTENT)
632
 
633
 
634
+ @app.get("/health")
635
+ async def health():
636
+ return {"status": "ok"}