Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>SQL Analyst OpenEnv · Playground</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,700;1,6..72,400&display=swap" rel="stylesheet" /> | |
| <style> | |
| :root { | |
| --bg: #0f1419; | |
| --surface: #1a222d; | |
| --border: #2d3a4a; | |
| --text: #e7ecf3; | |
| --muted: #8b9cb3; | |
| --accent: #3ddc97; | |
| --warn: #f5a623; | |
| --err: #ff6b6b; | |
| --sql-bg: #121820; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: "Newsreader", Georgia, serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| background-image: | |
| radial-gradient(ellipse 120% 80% at 100% -20%, rgba(61, 220, 151, 0.08), transparent), | |
| radial-gradient(ellipse 80% 50% at 0% 100%, rgba(45, 58, 74, 0.5), transparent); | |
| } | |
| .wrap { | |
| max-width: 1180px; | |
| margin: 0 auto; | |
| padding: 1.75rem 1.25rem 3rem; | |
| } | |
| header { | |
| display: flex; | |
| flex-wrap: wrap; | |
| align-items: baseline; | |
| justify-content: space-between; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| padding-bottom: 1rem; | |
| } | |
| h1 { | |
| font-size: 1.65rem; | |
| font-weight: 700; | |
| margin: 0; | |
| letter-spacing: -0.02em; | |
| } | |
| .tag { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.7rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.12em; | |
| color: var(--accent); | |
| border: 1px solid var(--border); | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 4px; | |
| } | |
| .links a { | |
| color: var(--muted); | |
| font-size: 0.9rem; | |
| margin-left: 1rem; | |
| } | |
| .links a:hover { color: var(--accent); } | |
| .grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1rem; | |
| } | |
| @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } } | |
| .panel { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 1rem 1.1rem; | |
| } | |
| .panel h2 { | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| color: var(--muted); | |
| margin: 0 0 0.75rem; | |
| } | |
| .task-meta { | |
| display: flex; | |
| gap: 0.5rem; | |
| flex-wrap: wrap; | |
| margin-bottom: 0.75rem; | |
| } | |
| .pill { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.72rem; | |
| padding: 0.2rem 0.5rem; | |
| border-radius: 4px; | |
| background: var(--sql-bg); | |
| border: 1px solid var(--border); | |
| } | |
| .pill.diff-easy { color: var(--accent); } | |
| .pill.diff-medium { color: var(--warn); } | |
| .pill.diff-hard { color: var(--err); } | |
| pre.schema, .instruction { | |
| font-size: 0.95rem; | |
| line-height: 1.55; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| margin: 0; | |
| color: var(--muted); | |
| } | |
| .instruction { color: var(--text); } | |
| textarea#sql { | |
| width: 100%; | |
| min-height: 140px; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.82rem; | |
| line-height: 1.5; | |
| background: var(--sql-bg); | |
| color: var(--text); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 0.85rem; | |
| resize: vertical; | |
| } | |
| textarea#sql:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 1px rgba(61, 220, 151, 0.25); | |
| } | |
| .actions { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.6rem; | |
| margin-top: 0.75rem; | |
| } | |
| button { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.78rem; | |
| padding: 0.55rem 1rem; | |
| border-radius: 6px; | |
| border: 1px solid var(--border); | |
| cursor: pointer; | |
| background: var(--surface); | |
| color: var(--text); | |
| } | |
| button.primary { | |
| background: rgba(61, 220, 151, 0.15); | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| button:hover { filter: brightness(1.12); } | |
| button:disabled { opacity: 0.45; cursor: not-allowed; } | |
| .metrics { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); | |
| gap: 0.5rem; | |
| margin-top: 0.75rem; | |
| } | |
| .metric { | |
| background: var(--sql-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| padding: 0.5rem 0.65rem; | |
| } | |
| .metric label { | |
| display: block; | |
| font-size: 0.65rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--muted); | |
| } | |
| .metric span { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 1rem; | |
| } | |
| .error { color: var(--err); font-size: 0.9rem; margin-top: 0.5rem; } | |
| .feedback { color: var(--warn); font-size: 0.9rem; margin-top: 0.35rem; } | |
| table.result { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.75rem; | |
| margin-top: 0.5rem; | |
| } | |
| table.result th, table.result td { | |
| border: 1px solid var(--border); | |
| padding: 0.35rem 0.5rem; | |
| text-align: left; | |
| } | |
| table.result th { background: var(--sql-bg); color: var(--muted); } | |
| .log { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.72rem; | |
| line-height: 1.5; | |
| max-height: 220px; | |
| overflow-y: auto; | |
| background: var(--sql-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 0.65rem; | |
| color: var(--muted); | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| } | |
| .full { grid-column: 1 / -1; } | |
| .status-bar { | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.72rem; | |
| color: var(--muted); | |
| margin-top: 0.5rem; | |
| } | |
| .status-bar.ok { color: var(--accent); } | |
| .status-bar.bad { color: var(--err); } | |
| .sql-hint { | |
| font-size: 0.88rem; | |
| line-height: 1.5; | |
| color: var(--muted); | |
| margin: 0 0 0.65rem; | |
| } | |
| .sql-hint code { font-family: "JetBrains Mono", monospace; font-size: 0.8rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <header> | |
| <div> | |
| <span class="tag">OpenEnv</span> | |
| <h1>SQL data analyst playground</h1> | |
| </div> | |
| <div class="links"> | |
| <a href="https://code.xeze.org/Hackathons/SQL-RL" target="_blank" rel="noopener noreferrer">Repository</a> | |
| <a href="/docs">API docs</a> | |
| <a href="/health">Health</a> | |
| </div> | |
| </header> | |
| <p class="status-bar" style="margin:-0.5rem 0 1.25rem;"> | |
| Interactive mode uses a persistent <strong>WebSocket</strong> session at <code>/ws</code> | |
| (OpenEnv HTTP <code>/step</code> is stateless). Connect, then run reset → SQL steps. | |
| The <strong>task</strong> is shown in natural language; each <strong>step</strong> must be SQL (OpenEnv action space). | |
| To drive the env from plain English automatically, use the <code>inference.py</code> LLM baseline — not this textarea. | |
| </p> | |
| <div class="grid"> | |
| <div class="panel"> | |
| <h2>Current task</h2> | |
| <div class="task-meta"> | |
| <span id="taskId" class="pill">—</span> | |
| <span id="difficulty" class="pill">—</span> | |
| </div> | |
| <p id="instruction" class="instruction">Click “Start episode” to load the first task.</p> | |
| </div> | |
| <div class="panel"> | |
| <h2>Schema</h2> | |
| <pre id="schema" class="schema">—</pre> | |
| </div> | |
| <div class="panel full"> | |
| <h2>Your SQL</h2> | |
| <p class="sql-hint"> | |
| Type <strong>only valid SQLite</strong> here. Natural language is not executed as SQL — English belongs in the task description (left), not in this box. | |
| </p> | |
| <textarea id="sql" placeholder="Executable SQL only, e.g. SELECT department_id, AVG(salary) ..."></textarea> | |
| <div class="actions"> | |
| <button type="button" class="primary" id="btnConnect">Connect session</button> | |
| <button type="button" class="primary" id="btnReset" disabled>Start episode (reset)</button> | |
| <button type="button" class="primary" id="btnStep" disabled>Run query (step)</button> | |
| <button type="button" id="btnState" disabled>Refresh state</button> | |
| </div> | |
| <div id="reqStatus" class="status-bar">Disconnected — click “Connect session”.</div> | |
| </div> | |
| <div class="panel"> | |
| <h2>Last step</h2> | |
| <div class="metrics"> | |
| <div class="metric"><label>Reward</label><span id="reward">—</span></div> | |
| <div class="metric"><label>Task score</label><span id="score">—</span></div> | |
| <div class="metric"><label>Episode done</label><span id="done">—</span></div> | |
| </div> | |
| <p id="graderFb" class="feedback" hidden></p> | |
| <p id="execErr" class="error" hidden></p> | |
| </div> | |
| <div class="panel"> | |
| <h2>Result preview</h2> | |
| <div id="resultTable"></div> | |
| </div> | |
| <div class="panel full"> | |
| <h2>Episode log</h2> | |
| <div id="log" class="log"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const $ = (id) => document.getElementById(id); | |
| const logEl = $("log"); | |
| function log(msg) { | |
| const t = new Date().toISOString().slice(11, 19); | |
| logEl.textContent += "[" + t + "] " + msg + "\n"; | |
| logEl.scrollTop = logEl.scrollHeight; | |
| } | |
| function setDifficultyClass(el, d) { | |
| el.className = "pill diff-" + (d || "").toLowerCase(); | |
| } | |
| function renderObservation(obs) { | |
| if (!obs) return; | |
| $("taskId").textContent = obs.task_id || "—"; | |
| const diff = obs.difficulty || ""; | |
| const diffEl = $("difficulty"); | |
| diffEl.textContent = diff || "—"; | |
| setDifficultyClass(diffEl, diff); | |
| $("instruction").textContent = obs.current_task_instruction || ""; | |
| $("schema").textContent = obs.schema_info || "—"; | |
| } | |
| function renderTableFromJson(jsonStr) { | |
| const host = $("resultTable"); | |
| host.innerHTML = ""; | |
| if (!jsonStr) { | |
| host.textContent = "No rows yet."; | |
| return; | |
| } | |
| let rows; | |
| try { rows = JSON.parse(jsonStr); } catch (e) { | |
| host.textContent = String(jsonStr).slice(0, 2000); | |
| return; | |
| } | |
| if (!Array.isArray(rows) || rows.length === 0) { | |
| host.textContent = "0 rows."; | |
| return; | |
| } | |
| const cols = Object.keys(rows[0]); | |
| const table = document.createElement("table"); | |
| table.className = "result"; | |
| const thead = document.createElement("thead"); | |
| const trh = document.createElement("tr"); | |
| cols.forEach((c) => { | |
| const th = document.createElement("th"); | |
| th.textContent = c; | |
| trh.appendChild(th); | |
| }); | |
| thead.appendChild(trh); | |
| table.appendChild(thead); | |
| const tbody = document.createElement("tbody"); | |
| rows.slice(0, 50).forEach((row) => { | |
| const tr = document.createElement("tr"); | |
| cols.forEach((c) => { | |
| const td = document.createElement("td"); | |
| const v = row[c]; | |
| td.textContent = v === null || v === undefined ? "" : String(v); | |
| tr.appendChild(td); | |
| }); | |
| tbody.appendChild(tr); | |
| }); | |
| table.appendChild(tbody); | |
| host.appendChild(table); | |
| if (rows.length > 50) { | |
| const note = document.createElement("p"); | |
| note.className = "status-bar"; | |
| note.textContent = "Showing first 50 rows."; | |
| host.appendChild(note); | |
| } | |
| } | |
| function setStatus(el, ok, text) { | |
| el.textContent = text; | |
| el.className = "status-bar " + (ok ? "ok" : "bad"); | |
| } | |
| let socket = null; | |
| function setConnected(connected) { | |
| $("btnReset").disabled = !connected; | |
| $("btnStep").disabled = !connected; | |
| $("btnState").disabled = !connected; | |
| $("btnConnect").textContent = connected ? "Reconnect" : "Connect session"; | |
| } | |
| function handleWsMessage(raw) { | |
| let msg; | |
| try { | |
| msg = JSON.parse(raw); | |
| } catch (e) { | |
| log("parse error: " + e); | |
| return; | |
| } | |
| const status = $("reqStatus"); | |
| if (msg.type === "error") { | |
| setStatus(status, false, msg.data && msg.data.message ? msg.data.message : "Server error"); | |
| log("error: " + JSON.stringify(msg.data)); | |
| return; | |
| } | |
| if (msg.type === "observation") { | |
| const pack = msg.data || {}; | |
| const obs = pack.observation; | |
| const rew = pack.reward; | |
| const done = pack.done; | |
| setStatus(status, true, "OK"); | |
| log("observation reward=" + rew + " done=" + done); | |
| $("reward").textContent = rew != null ? Number(rew).toFixed(3) : "—"; | |
| $("done").textContent = String(!!done); | |
| $("score").textContent = obs && obs.task_score != null ? Number(obs.task_score).toFixed(3) : "—"; | |
| renderObservation(obs); | |
| renderTableFromJson(obs && obs.execution_result); | |
| const fb = $("graderFb"); | |
| if (obs && obs.grader_feedback) { | |
| fb.textContent = obs.grader_feedback; | |
| fb.hidden = false; | |
| } else { fb.hidden = true; } | |
| const er = $("execErr"); | |
| if (obs && obs.execution_error) { | |
| er.textContent = obs.execution_error; | |
| er.hidden = false; | |
| } else { er.hidden = true; } | |
| return; | |
| } | |
| if (msg.type === "state") { | |
| setStatus(status, true, "State received (see log)."); | |
| log("state: " + JSON.stringify(msg.data).slice(0, 2000)); | |
| return; | |
| } | |
| log("unknown msg: " + raw.slice(0, 500)); | |
| } | |
| function connectWs() { | |
| if (socket && socket.readyState === WebSocket.OPEN) { | |
| socket.close(); | |
| } | |
| const proto = location.protocol === "https:" ? "wss:" : "ws:"; | |
| const url = proto + "//" + location.host + "/ws"; | |
| setStatus($("reqStatus"), true, "Connecting…"); | |
| socket = new WebSocket(url); | |
| socket.onopen = function () { | |
| setConnected(true); | |
| setStatus($("reqStatus"), true, "WebSocket connected. Run reset to start."); | |
| log("WebSocket open " + url); | |
| }; | |
| socket.onclose = function () { | |
| setConnected(false); | |
| setStatus($("reqStatus"), false, "Disconnected."); | |
| log("WebSocket closed"); | |
| }; | |
| socket.onerror = function () { | |
| setStatus($("reqStatus"), false, "WebSocket error."); | |
| log("WebSocket error"); | |
| }; | |
| socket.onmessage = function (ev) { | |
| handleWsMessage(ev.data); | |
| }; | |
| } | |
| function wsSend(obj) { | |
| if (!socket || socket.readyState !== WebSocket.OPEN) { | |
| setStatus($("reqStatus"), false, "Not connected."); | |
| return; | |
| } | |
| socket.send(JSON.stringify(obj)); | |
| } | |
| $("btnConnect").addEventListener("click", connectWs); | |
| $("btnReset").addEventListener("click", function () { | |
| wsSend({ type: "reset", data: {} }); | |
| $("graderFb").hidden = true; | |
| $("execErr").hidden = true; | |
| renderTableFromJson(null); | |
| log("sent reset"); | |
| }); | |
| $("btnStep").addEventListener("click", function () { | |
| const q = $("sql").value.trim(); | |
| if (!q) { | |
| setStatus($("reqStatus"), false, "Enter a SQL query."); | |
| return; | |
| } | |
| wsSend({ type: "step", data: { query: q } }); | |
| log("sent step"); | |
| }); | |
| $("btnState").addEventListener("click", function () { | |
| wsSend({ type: "state" }); | |
| log("sent state"); | |
| }); | |
| log("Open the Space, click Connect session, then Reset → run SQL."); | |
| </script> | |
| </body> | |
| </html> | |