sql-agent / server /playground.html
root
feat: add link to SQL-RL repository in playground.html for easy access
2269f6b
Raw
History Blame
15.8 kB
<!DOCTYPE html>
<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>