// SQLForge demo — frontend logic const $ = (id) => document.getElementById(id); const dbSelect = $("dbSelect"), questionEl = $("question"), examplesEl = $("examples"), schemaView = $("schemaView"), runBtn = $("runBtn"), placeholder = $("placeholder"), result = $("result"), sqlOut = $("sqlOut"), badges = $("badges"), traceBlock = $("traceBlock"), traceList = $("traceList"), resultsBlock = $("resultsBlock"), resultsHead = $("resultsHead"), resultsTable = $("resultsTable"), errorBlock = $("errorBlock"), errorOut = $("errorOut"); let EXAMPLES = [], modelReady = false; // ---------- model status ---------- async function pollHealth() { const el = $("status"), label = $("statusLabel"); try { const h = await (await fetch("/api/health")).json(); if (h.status === "online") { el.dataset.state = "online"; label.textContent = "model online"; modelReady = true; } else if (h.status === "loading") { el.dataset.state = "loading"; label.textContent = "loading model…"; modelReady = false; } else { el.dataset.state = "error"; label.textContent = "model error"; modelReady = false; } } catch { el.dataset.state = "offline"; label.textContent = "offline"; modelReady = false; } updateRunBtn(); } function updateRunBtn() { runBtn.disabled = !modelReady || runBtn.classList.contains("loading"); } // ---------- examples + schema ---------- async function loadExamples() { EXAMPLES = await (await fetch("/api/examples")).json(); dbSelect.innerHTML = EXAMPLES.map(e => ``).join(""); if (EXAMPLES.length) { await onDbChange(); } } async function onDbChange() { const ex = EXAMPLES.find(e => e.db_id === dbSelect.value); examplesEl.innerHTML = (ex?.questions || []) .map(q => `${esc(q)}`).join(""); examplesEl.querySelectorAll(".chip").forEach((c, i) => { c.onclick = () => { questionEl.value = ex.questions[i]; questionEl.focus(); }; }); try { const s = await (await fetch(`/api/schema?db_id=${encodeURIComponent(dbSelect.value)}`)).json(); schemaView.textContent = s.schema; } catch { schemaView.textContent = "—"; } } // ---------- generate ---------- async function run() { const question = questionEl.value.trim(); if (!question || !modelReady) return; runBtn.classList.add("loading"); updateRunBtn(); runBtn.querySelector(".btn-label").textContent = "Generating…"; try { const resp = await fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ question, db_id: dbSelect.value, self_correct: true }), }); if (!resp.ok) { const e = await resp.json().catch(() => ({})); throw new Error(e.detail || resp.statusText); } render(await resp.json()); } catch (err) { placeholder.classList.add("hidden"); result.classList.remove("hidden"); showError("Request failed", err.message); } finally { runBtn.classList.remove("loading"); updateRunBtn(); runBtn.querySelector(".btn-label").textContent = "Generate SQL"; } } function render(r) { placeholder.classList.add("hidden"); result.classList.remove("hidden"); sqlOut.innerHTML = highlight(r.sql); // badges const b = []; b.push(`${r.elapsed_s}s`); if (r.self_corrected) b.push(`🛠️ self-corrected (${r.attempts} tries)`); if (r.executed && !r.error) b.push(`✓ ran · ${r.row_count} row${r.row_count === 1 ? "" : "s"}`); if (r.executed && r.error) b.push(`✕ execution error`); badges.innerHTML = b.join(""); // self-correction trace (only show if there was more than one attempt) if (r.trace && r.trace.length > 1) { traceList.innerHTML = r.trace.map(step => { const ok = !step.error; return `
Attempt ${step.attempt} ${ok ? "ran clean" : "crashed"}
${highlight(step.sql)}
${step.error ? `
↳ ${esc(step.error)}
` : ""}
`; }).join(""); traceBlock.classList.remove("hidden"); } else { traceBlock.classList.add("hidden"); } // results / error errorBlock.classList.add("hidden"); resultsBlock.classList.add("hidden"); if (r.executed && r.error) { showError("Execution error", r.error); } else if (r.executed) { renderTable(r.columns, r.rows, r.row_count); } } function renderTable(cols, rows, n) { if (!cols || !cols.length) { resultsBlock.classList.add("hidden"); return; } resultsHead.textContent = `Query results — ${n} row${n === 1 ? "" : "s"}`; const head = `${cols.map(c => `${esc(c)}`).join("")}`; const body = `${rows.map(row => `${row.map(c => `${esc(c === null ? "NULL" : String(c))}`).join("")}`).join("")}`; resultsTable.innerHTML = head + (rows.length ? body : `(no rows returned)`); resultsBlock.classList.remove("hidden"); } function showError(title, msg) { document.querySelector("#errorBlock .block-head span").textContent = title; errorOut.textContent = msg; errorBlock.classList.remove("hidden"); } // ---------- tiny SQL highlighter ---------- const KW = /\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AS|AND|OR|NOT|IN|IS|NULL|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|DISTINCT|UNION|ALL|INTERSECT|EXCEPT|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|LIKE|BETWEEN|ASC|DESC|CASE|WHEN|THEN|ELSE|END|EXISTS)\b/gi; const FN = /\b(COUNT|SUM|AVG|MIN|MAX|ROUND|ABS|LENGTH|LOWER|UPPER|SUBSTR|COALESCE|CAST)\b/gi; function highlight(sql) { let h = esc(sql || ""); h = h.replace(/'([^']*)'/g, '\'$1\''); h = h.replace(/\b(\d+(\.\d+)?)\b/g, '$1'); h = h.replace(FN, (m) => `${m}`); h = h.replace(KW, (m) => `${m}`); return h; } function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); } // ---------- wire up ---------- dbSelect.onchange = onDbChange; runBtn.onclick = run; questionEl.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) run(); }); $("copyBtn").onclick = () => { navigator.clipboard.writeText(sqlOut.textContent).then(() => { const b = $("copyBtn"); b.textContent = "copied!"; setTimeout(() => b.textContent = "copy", 1200); }); }; loadExamples(); pollHealth(); setInterval(pollHealth, 4000);