// AutoAnalyst frontend — vanilla JS, no framework. const $ = (sel) => document.querySelector(sel); const state = { datasetId: null, sessionId: null, datasets: [], busy: false }; // ---- status pill (polls /api/health) ---------------------------------------- async function pollHealth() { const el = $("#status"); try { const h = await fetch("/api/health").then((r) => r.json()); const online = h.status === "online"; el.className = "status " + (online ? "online" : "offline"); el.querySelector(".label").textContent = online ? "Online" : "Offline"; el.title = online ? `model: ${h.model}` : (h.reason || "offline"); if (h.allow_upload) $("#uploadZone").classList.remove("hidden"); } catch { el.className = "status offline"; el.querySelector(".label").textContent = "Offline"; } } // ---- datasets ---------------------------------------------------------------- async function loadDatasets() { const grid = $("#datasetGrid"); try { state.datasets = await fetch("/api/datasets").then((r) => r.json()); } catch { grid.innerHTML = `

Could not load datasets.

`; return; } grid.innerHTML = ""; state.datasets.forEach((d) => { const card = document.createElement("button"); card.className = "ds-card"; card.innerHTML = `
${esc(d.label)}
${esc(d.description)}
`; card.onclick = () => selectDataset(d, card); grid.appendChild(card); }); } async function selectDataset(d, card) { document.querySelectorAll(".ds-card").forEach((c) => c.classList.remove("active")); card.classList.add("active"); state.datasetId = d.id; state.sessionId = null; renderChips(d.questions || []); $("#analyzeBtn").disabled = false; await loadSchema({ dataset_id: d.id }); } async function loadSchema(params) { const panel = $("#schemaPanel"); const url = params.dataset_id ? `/api/schema?dataset_id=${encodeURIComponent(params.dataset_id)}` : null; let s; try { s = url ? await fetch(url).then((r) => r.json()) : params.uploaded; } catch { return; } $("#schemaLabel").textContent = s.label || ""; $("#schemaMeta").textContent = `${s.row_count.toLocaleString()} rows · ${s.columns.length} columns`; const head = s.preview_columns || s.columns.map((c) => c.name); const rows = (s.preview || []).slice(0, 6); const dtypes = Object.fromEntries(s.columns.map((c) => [c.name, c.dtype])); let html = "" + head.map((c) => `${esc(c)}
${esc(dtypes[c] || "")}`).join("") + ""; rows.forEach((r) => { html += "" + r.map((v, i) => `${esc(fmt(v))}`).join("") + ""; }); html += ""; $("#schemaTable").innerHTML = html; panel.classList.remove("hidden"); } // ---- upload (local only) ----------------------------------------------------- $("#uploadBtn") && ($("#uploadBtn").onclick = () => $("#fileInput").click()); $("#fileInput") && ($("#fileInput").onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const body = new FormData(); body.append("file", file); try { const r = await fetch("/api/upload", { method: "POST", body }); if (!r.ok) throw new Error((await r.json()).detail || "upload failed"); const s = await r.json(); state.sessionId = s.session_id; state.datasetId = null; document.querySelectorAll(".ds-card").forEach((c) => c.classList.remove("active")); renderChips([]); $("#analyzeBtn").disabled = false; await loadSchema({ uploaded: s }); } catch (err) { showBanner(`Upload failed: ${err.message}`); } }); // ---- example chips ----------------------------------------------------------- function renderChips(questions) { const wrap = $("#chips"); wrap.innerHTML = ""; questions.forEach((q) => { const c = document.createElement("button"); c.className = "chip"; c.textContent = q; c.onclick = () => { $("#question").value = q; analyze(); }; wrap.appendChild(c); }); } // ---- analyze ----------------------------------------------------------------- $("#analyzeBtn").onclick = analyze; $("#question").addEventListener("keydown", (e) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") analyze(); }); let stream = null; function analyze() { if (state.busy) return; const question = $("#question").value.trim(); if (!question) return; if (!state.datasetId && !state.sessionId) { showBanner("Pick a dataset (or upload one) first."); return; } setBusy(true); const results = $("#results"); results.innerHTML = ""; const thinking = document.createElement("div"); thinking.className = "thinking"; thinking.innerHTML = `AutoAnalyst is reading the data…`; const setThinking = (t) => thinking.querySelector(".t-label").textContent = t; results.appendChild(thinking); const params = new URLSearchParams({ question }); if (state.datasetId) params.set("dataset_id", state.datasetId); else params.set("session_id", state.sessionId); let finished = false; const done = () => { finished = true; thinking.remove(); stream.close(); setBusy(false); }; stream = new EventSource(`/api/analyze/stream?${params}`); stream.onmessage = (ev) => { let msg; try { msg = JSON.parse(ev.data); } catch { return; } if (msg.type === "step") { results.insertBefore(renderStep(msg), thinking); setThinking(`Ran step ${msg.n} — deciding the next move…`); } else if (msg.type === "final") { done(); results.appendChild(renderAnswer(msg)); } else if (msg.type === "error") { done(); showBanner(msg.detail || "analysis failed"); } }; stream.onerror = () => { if (finished) return; done(); showBanner("Connection lost while analyzing. Is the model online?"); }; } function renderResult(data) { const results = $("#results"); results.innerHTML = ""; (data.trace || []).forEach((step) => results.appendChild(renderStep(step))); results.appendChild(renderAnswer(data)); } function renderStep(step) { const el = document.createElement("div"); el.className = "step"; let inner = `
${step.n} run_python · step ${step.n}
${highlight(step.code || "")}
`; const io = []; if (step.stdout && step.stdout.trim()) io.push(`
Output
${esc(step.stdout.trim())}
`); if (step.error) io.push(`
Error
${esc(step.error.trim())}
`); if (io.length) inner += `
${io.join("")}
`; (step.charts || []).forEach((c) => { inner += `
chart from step ${step.n}
`; }); el.innerHTML = inner; return el; } function renderAnswer(data) { const el = document.createElement("div"); el.className = "answer"; const meta = data.stopped === "step_limit" ? `reached the ${data.steps_used}-step limit` : `${data.steps_used} step${data.steps_used === 1 ? "" : "s"}`; el.innerHTML = `
Answer
${esc(data.answer || "(no answer)")}
${esc(data.label || "")} · ${meta}
`; return el; } // ---- helpers ----------------------------------------------------------------- function setBusy(b) { state.busy = b; const btn = $("#analyzeBtn"); btn.classList.toggle("busy", b); btn.disabled = b; } function showBanner(msg) { const results = $("#results"); const b = document.createElement("div"); b.className = "banner-err"; b.innerHTML = ` ${esc(msg)}`; results.prepend(b); } function esc(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function fmt(v) { if (v === null || v === undefined) return "—"; if (typeof v === "number") return Number.isInteger(v) ? v : v.toFixed(2); return v; } // minimal Python syntax highlighter — single-pass tokenizer so HTML entities // are never re-processed (each token is escaped exactly once before wrapping). const PY_KW = new Set(["import", "from", "as", "def", "return", "if", "elif", "else", "for", "while", "in", "not", "and", "or", "is", "None", "True", "False", "lambda", "with", "print"]); const PY_BI = new Set(["df", "pd", "np", "plt", "sum", "mean", "count", "groupby", "sort_values", "head", "describe", "value_counts", "plot", "title", "xlabel", "ylabel", "len", "round", "sorted", "agg", "merge", "pivot_table"]); function highlight(code) { const re = /(#[^\n]*)|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|(\b\d+\.?\d*\b)|([A-Za-z_]\w*)/g; let out = "", last = 0, m; while ((m = re.exec(code))) { out += esc(code.slice(last, m.index)); const [full, com, str, num, ident] = m; if (com) out += `${esc(com)}`; else if (str) out += `${esc(str)}`; else if (num) out += `${num}`; else if (PY_KW.has(ident)) out += `${ident}`; else if (PY_BI.has(ident)) out += `${ident}`; else out += esc(ident); last = re.lastIndex; } out += esc(code.slice(last)); return out; } // ---- boot -------------------------------------------------------------------- pollHealth(); setInterval(pollHealth, 15000); loadDatasets();