CoolFace
Apppublic

dkAmulet/sql-query-optimizer

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
index.html555 linesDownload Raw Back to static
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="UTF-8" />5<meta name="viewport" content="width=device-width, initial-scale=1.0" />6<title>SQL Query Optimizer — OpenEnv</title>7<style>8  :root {9    --bg:       #0f1117;10    --surface:  #1a1d27;11    --border:   #2a2d3e;12    --accent:   #4f8ef7;13    --accent2:  #7c5cf7;14    --green:    #22c55e;15    --yellow:   #eab308;16    --red:      #ef4444;17    --text:     #e2e8f0;18    --muted:    #64748b;19    --radius:   10px;20    --mono:     'JetBrains Mono', 'Fira Code', monospace;21  }22  * { box-sizing: border-box; margin: 0; padding: 0; }23  body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; min-height: 100vh; }24 25  /* ── Header ── */26  header {27    background: linear-gradient(135deg, #1a1d27 0%, #0f1117 100%);28    border-bottom: 1px solid var(--border);29    padding: 20px 32px;30    display: flex; align-items: center; justify-content: space-between;31  }32  .logo { display: flex; align-items: center; gap: 12px; }33  .logo-icon { font-size: 28px; }34  .logo h1 { font-size: 20px; font-weight: 700; background: linear-gradient(90deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }35  .logo p { font-size: 12px; color: var(--muted); }36  .badges { display: flex; gap: 8px; }37  .badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; letter-spacing: .5px; }38  .badge-blue  { background: rgba(79,142,247,.15); color: var(--accent); border: 1px solid rgba(79,142,247,.3); }39  .badge-green { background: rgba(34,197,94,.15);  color: var(--green);  border: 1px solid rgba(34,197,94,.3); }40 41  /* ── Layout ── */42  .main { display: grid; grid-template-columns: 340px 1fr; gap: 0; height: calc(100vh - 73px); }43 44  /* ── Sidebar ── */45  .sidebar {46    background: var(--surface);47    border-right: 1px solid var(--border);48    overflow-y: auto;49    padding: 20px;50    display: flex; flex-direction: column; gap: 20px;51  }52  .section-label { font-size: 11px; font-weight: 700; letter-spacing: 1px; color: var(--muted); text-transform: uppercase; margin-bottom: 8px; }53 54  /* Task cards */55  .task-card {56    background: var(--bg);57    border: 1.5px solid var(--border);58    border-radius: var(--radius);59    padding: 14px;60    cursor: pointer;61    transition: all .15s;62  }63  .task-card:hover { border-color: var(--accent); }64  .task-card.active { border-color: var(--accent); background: rgba(79,142,247,.05); }65  .task-card-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px; }66  .task-name { font-size: 13px; font-weight: 600; }67  .diff-badge { padding: 2px 8px; border-radius: 20px; font-size: 10px; font-weight: 700; }68  .diff-easy   { background: rgba(34,197,94,.15); color: var(--green); }69  .diff-medium { background: rgba(234,179,8,.15);  color: var(--yellow); }70  .diff-hard   { background: rgba(239,68,68,.15);  color: var(--red); }71  .task-desc { font-size: 11px; color: var(--muted); line-height: 1.5; }72 73  /* Schema */74  .schema-box {75    background: var(--bg);76    border: 1px solid var(--border);77    border-radius: var(--radius);78    padding: 12px;79    font-family: var(--mono);80    font-size: 10.5px;81    color: #94a3b8;82    white-space: pre;83    overflow-x: auto;84    max-height: 220px;85    overflow-y: auto;86    line-height: 1.5;87  }88 89  /* ── Main panel ── */90  .panel { display: flex; flex-direction: column; overflow: hidden; }91 92  .top-row {93    padding: 16px 24px;94    border-bottom: 1px solid var(--border);95    display: flex; gap: 16px; align-items: flex-start;96  }97  .query-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }98  .query-label { font-size: 11px; font-weight: 700; letter-spacing: 1px; color: var(--muted); text-transform: uppercase; }99  .slow-query {100    background: var(--bg);101    border: 1px solid var(--border);102    border-radius: 8px;103    padding: 12px;104    font-family: var(--mono);105    font-size: 12px;106    color: #f87171;107    white-space: pre;108    line-height: 1.6;109  }110 111  /* Editor */112  .editor-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; padding: 16px 24px; gap: 12px; }113  .editor-header { display: flex; justify-content: space-between; align-items: center; }114  .editor-label { font-size: 11px; font-weight: 700; letter-spacing: 1px; color: var(--muted); text-transform: uppercase; }115  .editor-actions { display: flex; gap: 8px; }116 117  textarea#sql-input {118    flex: 1;119    background: var(--bg);120    border: 1.5px solid var(--border);121    border-radius: var(--radius);122    color: var(--text);123    font-family: var(--mono);124    font-size: 13px;125    padding: 14px;126    resize: none;127    line-height: 1.7;128    outline: none;129    transition: border-color .15s;130    min-height: 160px;131  }132  textarea#sql-input:focus { border-color: var(--accent); }133 134  /* Buttons */135  .btn {136    padding: 8px 18px;137    border-radius: 8px;138    font-size: 13px;139    font-weight: 600;140    cursor: pointer;141    border: none;142    transition: all .15s;143    display: flex; align-items: center; gap: 6px;144  }145  .btn-primary { background: var(--accent); color: #fff; }146  .btn-primary:hover { background: #3b7fea; }147  .btn-primary:disabled { opacity: .4; cursor: not-allowed; }148  .btn-ghost { background: transparent; color: var(--muted); border: 1px solid var(--border); }149  .btn-ghost:hover { color: var(--text); border-color: var(--text); }150  .btn-sm { padding: 5px 12px; font-size: 12px; }151 152  /* Results */153  .results-area {154    border-top: 1px solid var(--border);155    padding: 16px 24px;156    display: flex; flex-direction: column; gap: 12px;157    background: rgba(0,0,0,.2);158    min-height: 180px;159  }160 161  .score-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }162  .total-score {163    font-size: 40px;164    font-weight: 800;165    font-family: var(--mono);166    background: linear-gradient(90deg, var(--accent), var(--accent2));167    -webkit-background-clip: text; -webkit-text-fill-color: transparent;168  }169  .score-label { font-size: 11px; color: var(--muted); margin-top: 4px; }170 171  .breakdown { display: flex; gap: 12px; flex-wrap: wrap; flex: 1; }172  .dim {173    background: var(--surface);174    border: 1px solid var(--border);175    border-radius: 8px;176    padding: 10px 14px;177    min-width: 100px;178  }179  .dim-name { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; }180  .dim-bar-bg { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }181  .dim-bar { height: 100%; border-radius: 2px; transition: width .4s ease; }182  .dim-val { font-size: 15px; font-weight: 700; font-family: var(--mono); margin-top: 4px; }183 184  .feedback-box {185    background: var(--bg);186    border: 1px solid var(--border);187    border-radius: 8px;188    padding: 10px 14px;189    font-size: 12px;190    color: var(--muted);191    line-height: 1.6;192  }193  .feedback-box.has-content { color: var(--text); }194 195  /* Step progress */196  .progress-bar { display: flex; gap: 4px; }197  .step-pip { width: 20px; height: 6px; border-radius: 3px; background: var(--border); transition: background .3s; }198  .step-pip.done { background: var(--accent); }199  .step-pip.current { background: var(--accent2); }200 201  /* History */202  .history-list { max-height: 80px; overflow-y: auto; display: flex; flex-direction: column; gap: 3px; }203  .history-item { font-size: 11px; font-family: var(--mono); color: var(--muted); display: flex; gap: 8px; }204  .history-item .h-step { color: var(--accent); min-width: 44px; }205  .history-item .h-score { font-weight: 600; }206  .history-item .h-score.good { color: var(--green); }207  .history-item .h-score.mid  { color: var(--yellow); }208  .history-item .h-score.bad  { color: var(--red); }209 210  /* Placeholder */211  .placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 8px; color: var(--muted); font-size: 13px; }212  .placeholder .icon { font-size: 32px; opacity: .4; }213 214  /* Loader */215  .spinner { width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.2); border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; }216  @keyframes spin { to { transform: rotate(360deg); } }217 218  /* Scrollbars */219  ::-webkit-scrollbar { width: 5px; height: 5px; }220  ::-webkit-scrollbar-track { background: transparent; }221  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }222</style>223</head>224<body>225 226<header>227  <div class="logo">228    <div class="logo-icon">🗄️</div>229    <div>230      <h1>SQL Query Optimizer</h1>231      <p>OpenEnv · RL Environment for SQL Rewriting</p>232    </div>233  </div>234  <div class="badges">235    <span class="badge badge-blue">OpenEnv v1.0</span>236    <span class="badge badge-green">Live</span>237  </div>238</header>239 240<div class="main">241  <!-- ── Sidebar ── -->242  <aside class="sidebar">243    <div>244      <div class="section-label">Tasks</div>245      <div id="task-list" style="display:flex;flex-direction:column;gap:8px;"></div>246    </div>247 248    <div>249      <div class="section-label">Episode Progress</div>250      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">251        <span id="step-counter" style="font-size:12px;color:var(--muted)">Step 0 / 0</span>252        <span id="best-reward-label" style="font-size:12px;font-family:var(--mono);color:var(--accent)">Best: —</span>253      </div>254      <div class="progress-bar" id="progress-bar"></div>255    </div>256 257    <div>258      <div class="section-label">Step History</div>259      <div class="history-list" id="history-list">260        <div style="font-size:11px;color:var(--muted)">No steps yet.</div>261      </div>262    </div>263 264    <div>265      <div class="section-label">Database Schema</div>266      <div class="schema-box" id="schema-box">Loading…</div>267    </div>268  </aside>269 270  <!-- ── Main Panel ── -->271  <div class="panel">272    <!-- Slow query -->273    <div class="top-row">274      <div class="query-col">275        <div class="query-label">⚠ Slow Query (to optimise)</div>276        <div class="slow-query" id="slow-query-box">Select a task →</div>277      </div>278    </div>279 280    <!-- Editor -->281    <div class="editor-area">282      <div class="editor-header">283        <div class="editor-label">✏ Your Optimised Query</div>284        <div class="editor-actions">285          <button class="btn btn-ghost btn-sm" onclick="clearEditor()">Clear</button>286          <button class="btn btn-ghost btn-sm" onclick="pasteSlowQuery()">Paste slow query</button>287          <button class="btn btn-primary" id="submit-btn" onclick="submitQuery()" disabled>288            <span id="submit-label">Submit</span>289          </button>290        </div>291      </div>292      <textarea id="sql-input" spellcheck="false"293        placeholder="-- Write your optimised SQL here…294-- Tip: select a task on the left first, then click Submit to grade it."></textarea>295    </div>296 297    <!-- Results -->298    <div class="results-area">299      <div id="results-placeholder" class="placeholder">300        <div class="icon">📊</div>301        <div>Submit a query to see your score</div>302      </div>303      <div id="results-content" style="display:none;flex-direction:column;gap:10px;">304        <div class="score-row">305          <div>306            <div class="total-score" id="total-score">0.000</div>307            <div class="score-label">Total Score</div>308          </div>309          <div class="breakdown">310            <div class="dim">311              <div class="dim-name">Validity</div>312              <div class="dim-bar-bg"><div class="dim-bar" id="bar-validity" style="width:0%;background:#4f8ef7;"></div></div>313              <div class="dim-val" id="val-validity">0.00</div>314            </div>315            <div class="dim">316              <div class="dim-name">Correctness</div>317              <div class="dim-bar-bg"><div class="dim-bar" id="bar-correctness" style="width:0%;background:#7c5cf7;"></div></div>318              <div class="dim-val" id="val-correctness">0.00</div>319            </div>320            <div class="dim">321              <div class="dim-name">Performance</div>322              <div class="dim-bar-bg"><div class="dim-bar" id="bar-performance" style="width:0%;background:#22c55e;"></div></div>323              <div class="dim-val" id="val-performance">0.00</div>324            </div>325            <div class="dim">326              <div class="dim-name">Style</div>327              <div class="dim-bar-bg"><div class="dim-bar" id="bar-style" style="width:0%;background:#eab308;"></div></div>328              <div class="dim-val" id="val-style">0.00</div>329            </div>330          </div>331        </div>332        <div class="feedback-box" id="feedback-box">—</div>333      </div>334    </div>335  </div>336</div>337 338<script>339const API = '';   // same origin340let currentTask  = null;341let currentSlowQ = '';342let stepNum      = 0;343let maxSteps     = 0;344let bestReward   = 0;345let history      = [];346 347const TASK_HINTS = {348  select_star_removal:    'SELECT user_id, username, email\nFROM users\nWHERE is_active = 1',349  subquery_to_join:       'SELECT o.order_id, o.user_id, o.total_amount\nFROM orders o\nJOIN users u ON u.user_id = o.user_id\nWHERE u.country = \'USA\'\n  AND u.is_active = 1\n  AND o.status = \'delivered\'',350  aggregation_optimization: 'SELECT   c.name AS category_name,\n         SUM(oi.quantity * oi.unit_price) AS total_revenue\nFROM     categories c\nJOIN     products p ON p.category_id = c.category_id\nJOIN     order_items oi ON oi.product_id = p.product_id\nGROUP BY c.category_id, c.name\nHAVING   SUM(oi.quantity * oi.unit_price) > 1000\nORDER BY total_revenue DESC',351};352 353// ── Boot ──────────────────────────────────────────────────────────────────────354async function init() {355  const res  = await fetch(`${API}/tasks`);356  const tasks = await res.json();357  renderTaskList(tasks);358}359 360function renderTaskList(tasks) {361  const el = document.getElementById('task-list');362  el.innerHTML = '';363  tasks.forEach(t => {364    const card = document.createElement('div');365    card.className = 'task-card';366    card.id = `task-card-${t.task_id}`;367    card.innerHTML = `368      <div class="task-card-top">369        <div class="task-name">${t.name}</div>370        <span class="diff-badge diff-${t.difficulty}">${t.difficulty}</span>371      </div>372      <div class="task-desc">${t.description.substring(0,120)}…</div>`;373    card.onclick = () => selectTask(t);374    el.appendChild(card);375  });376}377 378// ── Task selection ─────────────────────────────────────────────────────────────379async function selectTask(t) {380  // Highlight card381  document.querySelectorAll('.task-card').forEach(c => c.classList.remove('active'));382  document.getElementById(`task-card-${t.task_id}`).classList.add('active');383 384  // Reset state385  history = [];386  bestReward = 0;387  renderHistory();388  hideResults();389 390  // Call /reset391  const res  = await fetch(`${API}/reset?task_id=${t.task_id}`, { method: 'POST' });392  const obs  = await res.json();393 394  currentTask  = t.task_id;395  currentSlowQ = obs.slow_query;396  stepNum      = 0;397  maxSteps     = obs.max_steps;398 399  document.getElementById('slow-query-box').textContent = obs.slow_query;400  document.getElementById('schema-box').textContent     = obs.schema_ddl;401  document.getElementById('sql-input').value            = '';402  document.getElementById('sql-input').placeholder      = `-- Write your optimised SQL for "${t.name}"…`;403  document.getElementById('submit-btn').disabled        = false;404 405  updateProgress();406  document.getElementById('best-reward-label').textContent = 'Best: 0.000';407}408 409// ── Submit ─────────────────────────────────────────────────────────────────────410async function submitQuery() {411  const q = document.getElementById('sql-input').value.trim();412  if (!q || !currentTask) return;413 414  const btn   = document.getElementById('submit-btn');415  const label = document.getElementById('submit-label');416  btn.disabled = true;417  label.innerHTML = '<div class="spinner"></div>';418 419  try {420    const res    = await fetch(`${API}/step`, {421      method: 'POST',422      headers: { 'Content-Type': 'application/json' },423      body: JSON.stringify({ optimized_query: q }),424    });425    const result = await res.json();426 427    if (res.status === 400) {428      alert(result.detail || 'Error — reset the task and try again.');429      return;430    }431 432    stepNum++;433    const r   = result.reward;434    const bd  = r.breakdown;435 436    // Update best437    if (r.value > bestReward) bestReward = r.value;438 439    // History440    history.push({ step: stepNum, score: r.value, feedback: r.feedback });441    renderHistory();442 443    // Results444    showResults(r.value, bd, r.feedback);445    updateProgress();446 447    document.getElementById('best-reward-label').textContent =448      `Best: ${bestReward.toFixed(3)}`;449 450    if (result.done) {451      btn.disabled = true;452      label.textContent = 'Episode done';453      showToast(r.value >= 0.95 ? '🎉 Perfect score!' : `Episode complete — best ${bestReward.toFixed(3)}`);454    }455  } finally {456    if (!document.getElementById('submit-btn').disabled) {457      label.textContent = 'Submit';458      btn.disabled = false;459    } else if (label.textContent !== 'Episode done') {460      label.textContent = 'Submit';461      btn.disabled = false;462    }463  }464}465 466// ── UI helpers ────────────────────────────────────────────────────────────────467function showResults(total, bd, feedback) {468  document.getElementById('results-placeholder').style.display = 'none';469  const rc = document.getElementById('results-content');470  rc.style.display = 'flex';471 472  document.getElementById('total-score').textContent = total.toFixed(3);473 474  const dims = { validity: bd.validity, correctness: bd.correctness, performance: bd.performance, style: bd.style };475  // Max values per dim (approximate — for bar scaling)476  const maxes = { validity: 0.10, correctness: 0.40, performance: 0.30, style: 0.30 };477  for (const [k, v] of Object.entries(dims)) {478    document.getElementById(`val-${k}`).textContent = v.toFixed(2);479    const pct = Math.round((v / maxes[k]) * 100);480    document.getElementById(`bar-${k}`).style.width = `${Math.min(pct, 100)}%`;481  }482 483  const fb = document.getElementById('feedback-box');484  fb.textContent = feedback || '—';485  fb.className   = `feedback-box ${feedback ? 'has-content' : ''}`;486}487 488function hideResults() {489  document.getElementById('results-placeholder').style.display = 'flex';490  document.getElementById('results-content').style.display = 'none';491}492 493function updateProgress() {494  document.getElementById('step-counter').textContent = `Step ${stepNum} / ${maxSteps}`;495  const bar = document.getElementById('progress-bar');496  bar.innerHTML = '';497  for (let i = 0; i < maxSteps; i++) {498    const pip = document.createElement('div');499    pip.className = 'step-pip' + (i < stepNum ? ' done' : i === stepNum ? ' current' : '');500    bar.appendChild(pip);501  }502}503 504function renderHistory() {505  const el = document.getElementById('history-list');506  if (!history.length) {507    el.innerHTML = '<div style="font-size:11px;color:var(--muted)">No steps yet.</div>';508    return;509  }510  el.innerHTML = [...history].reverse().map(h => {511    const cls = h.score >= 0.80 ? 'good' : h.score >= 0.45 ? 'mid' : 'bad';512    const short = h.feedback ? h.feedback.substring(0, 60) + (h.feedback.length > 60 ? '…' : '') : '';513    return `<div class="history-item">514      <span class="h-step">Step ${h.step}</span>515      <span class="h-score ${cls}">${h.score.toFixed(3)}</span>516      <span style="color:var(--muted);font-size:10px">${short}</span>517    </div>`;518  }).join('');519}520 521function clearEditor() {522  document.getElementById('sql-input').value = '';523  document.getElementById('sql-input').focus();524}525 526function pasteSlowQuery() {527  document.getElementById('sql-input').value = currentSlowQ || '';528  document.getElementById('sql-input').focus();529}530 531function showToast(msg) {532  const t = document.createElement('div');533  t.textContent = msg;534  Object.assign(t.style, {535    position: 'fixed', bottom: '24px', right: '24px',536    background: 'var(--surface)', border: '1px solid var(--border)',537    borderRadius: '10px', padding: '12px 20px', fontSize: '13px',538    boxShadow: '0 8px 24px rgba(0,0,0,.4)', zIndex: 999,539    animation: 'fadeIn .2s ease',540    color: 'var(--text)',541  });542  document.body.appendChild(t);543  setTimeout(() => t.remove(), 3500);544}545 546// Submit on Ctrl/Cmd+Enter547document.addEventListener('keydown', e => {548  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') submitQuery();549});550 551init();552</script>553</body>554</html>555