Files
sql-server-query-ai/components/runner.tsx
T
2026-09-22 08:32:40 +03:00

75 lines
4.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { readSse } from "@/lib/stream";
import { ReportView } from "./report-view";
import { ExportButtons } from "./export-buttons";
const STAGES = ["digesting", "proposing", "judging", "writing"] as const;
const LABEL: Record<string, string> = { digesting: "Reading the plan", proposing: "Asking for options",
judging: "Jev is scoring", writing: "Writing the report" };
export function Runner() {
const [xml, setXml] = useState(""); const [note, setNote] = useState("");
const [stage, setStage] = useState<string | null>(null);
const [oul, setOul] = useState(""); const [error, setError] = useState<string | null>(null);
const at = stage ? STAGES.indexOf(stage as (typeof STAGES)[number]) : -1;
async function run() {
setError(null); setOul(""); setStage("digesting");
try {
const res = await fetch("/api/analyze", { method: "POST",
headers: { "content-type": "application/json" }, body: JSON.stringify({ xml, note }) });
if (!res.ok || !res.body) { setError((await res.json().catch(() => null))?.error ?? `Request failed (${res.status}).`);
return setStage(null); }
for await (const e of readSse(res.body)) {
if (e.event === "stage") setStage(e.data.stage);
else if (e.event === "oul") setOul((s) => s + e.data.chunk);
else if (e.event === "error") setError(`${e.data.stage}: ${e.data.message}`);
else if (e.event === "done") { setStage(null); history.replaceState(null, "", `/app/${e.data.id}`); }
}
} catch (e) {
setError((e as Error).message || "The connection to the server dropped.");
setStage(null);
}
}
return <section style={{ display: "grid", gap: "var(--qai-space-xl)", minWidth: 0 }}>
<h1 style={{ fontFamily: "var(--qai-font-display)", fontSize: "var(--qai-text-display)",
letterSpacing: "-0.02em", margin: 0 }}>Paste a plan</h1>
<details className="card">
<summary style={{ cursor: "pointer", fontFamily: "var(--qai-font-display)", fontSize: "var(--qai-text-sm)" }}>
Where do I get this XML?</summary>
<ol style={{ fontSize: "var(--qai-text-sm)", color: "var(--qai-ink-muted)", margin: "var(--qai-space-md) 0 0",
paddingLeft: "1.2em" }}>
<li>SSMS: press <strong>Ctrl+M</strong> (Include Actual Execution Plan), then run the query.</li>
<li>On the Execution Plan tab, right-click → <strong>Save As .sqlplan</strong>. Or right-click the statement → <strong>Showplan XML</strong> and copy the cell.</li>
<li>No SSMS: prefix the batch with <code>SET STATISTICS XML ON;</code> and copy the XML result cell.</li>
<li>Drop the file or paste its contents below. Only the digest leaves this container, and only to your own models.</li>
</ol>
</details>
<div className="runner card" style={{ display: "grid", gap: "var(--qai-space-md)" }}>
<label className="label" htmlFor="xml">ShowPlan XML or .sqlplan contents</label>
<textarea className="field" id="xml" rows={7} spellCheck={false} value={xml}
onChange={(e) => setXml(e.target.value)} placeholder="<ShowPlanXML …>" />
<label className="label" htmlFor="note">Anything the plan cannot see? (optional)</label>
<input className="field" id="note" value={note} onChange={(e) => setNote(e.target.value)}
placeholder="slow since the 16.0.4085 migration" />
<div style={{ display: "flex", gap: "var(--qai-space-s)", alignItems: "center", flexWrap: "wrap" }}>
<button className="btn btn-primary" onClick={run} disabled={!xml.trim() || stage !== null}>Analyse</button>
<input type="file" accept=".sqlplan,.xml" aria-label="Load a .sqlplan file" style={{ fontSize: "var(--qai-text-xs)" }}
onChange={async (e) => {
const f = e.target.files?.[0]; if (!f) return;
const buf = new Uint8Array(await f.arrayBuffer());
// SSMS saves UTF-16LE; File.text() decodes that as UTF-8 and fills the plan with NUL bytes.
const utf16 = buf[0] === 0xff && buf[1] === 0xfe;
setXml(new TextDecoder(utf16 ? "utf-16le" : "utf-8").decode(buf).replace(/^/, ""));
}} />
{at >= 0 && <span className="pill" aria-live="polite">{at + 1} of 4 · {LABEL[STAGES[at]]}</span>}
</div>
{error && <p role="alert" style={{ color: "var(--qai-danger)", margin: 0 }}>{error}</p>}
</div>
{oul && <ExportButtons oul={oul} title={note || "Query plan analysis"} />}
<ReportView oul={oul} streaming={stage !== null} />
</section>;
}