File size: 6,722 Bytes
d6bfc8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// 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 => `<option value="${e.db_id}">${e.label}</option>`).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 => `<span class="chip" title="${esc(q)}">${esc(q)}</span>`).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(`<span class="badge">${r.elapsed_s}s</span>`);
  if (r.self_corrected) b.push(`<span class="badge fix">🛠️ self-corrected (${r.attempts} tries)</span>`);
  if (r.executed && !r.error) b.push(`<span class="badge good">✓ ran · ${r.row_count} row${r.row_count === 1 ? "" : "s"}</span>`);
  if (r.executed && r.error) b.push(`<span class="badge warn">✕ execution error</span>`);
  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 `<div class="trace-step">
        <div class="tlabel">Attempt ${step.attempt}
          <span class="tag ${ok ? "ok" : "fail"}">${ok ? "ran clean" : "crashed"}</span></div>
        <pre class="code sql mono">${highlight(step.sql)}</pre>
        ${step.error ? `<div class="terr">↳ ${esc(step.error)}</div>` : ""}
      </div>`;
    }).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 = `<thead><tr>${cols.map(c => `<th>${esc(c)}</th>`).join("")}</tr></thead>`;
  const body = `<tbody>${rows.map(row =>
    `<tr>${row.map(c => `<td>${esc(c === null ? "NULL" : String(c))}</td>`).join("")}</tr>`).join("")}</tbody>`;
  resultsTable.innerHTML = head + (rows.length ? body : `<tbody><tr><td colspan="${cols.length}" class="muted">(no rows returned)</td></tr></tbody>`);
  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, '<span class="str">\'$1\'</span>');
  h = h.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="num">$1</span>');
  h = h.replace(FN, (m) => `<span class="fn">${m}</span>`);
  h = h.replace(KW, (m) => `<span class="kw">${m}</span>`);
  return h;
}
function esc(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }

// ---------- 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);