diff --git a/README.md b/README.md index ee4bdcf..6ac8881 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ brings their own endpoint and their own spend. Blank key field = keep the stored - Analyst: any OpenAI-compatible base URL + key + model, plus optional extra JSON params. A local endpoint such as `http://192.168.88.131:8888/v1` works; use key `EMPTY` when the server has no auth. + For a reasoning model (Qwen3 on vLLM) put `{"chat_template_kwargs":{"enable_thinking":false}}` in **Extra params**: + with thinking on, the model spends the whole context budget reasoning and the report arrives empty. + Both model calls default to `max_tokens: 2048`; the same field overrides it. - Jev: a TypeSafe API key from `console.typesafe.ai/keys`, model usually `jev-latest`. ## Tests diff --git a/app/app/[id]/page.tsx b/app/app/[id]/page.tsx new file mode 100644 index 0000000..b9624d7 --- /dev/null +++ b/app/app/[id]/page.tsx @@ -0,0 +1,20 @@ +import { notFound, redirect } from "next/navigation"; +import { requireUser } from "@/lib/auth"; +import { getAnalysis } from "@/lib/db"; +import { ReportView } from "@/components/report-view"; +import { ExportButtons } from "@/components/export-buttons"; +import { VerdictSummary } from "@/components/verdict-summary"; + +export default async function Stored({ params }: { params: Promise<{ id: string }> }) { + const u = await requireUser(); + const a = getAnalysis((await params).id, u.id); + if (!a) notFound(); + if (a.status === "running") redirect(`/app/${a.id}`); // still streaming elsewhere; rare, bounce once + return
+

{a.title}

+ {a.error &&

{a.error}

} + {a.verdict && } + {a.oul ? <> + :

No report was stored for this run.

} +
; +} diff --git a/app/app/layout.tsx b/app/app/layout.tsx index 2562259..9ca239c 100644 --- a/app/app/layout.tsx +++ b/app/app/layout.tsx @@ -1,9 +1,10 @@ import { requireUser } from "@/lib/auth"; +import { Rail } from "@/components/rail"; export default async function Console({ children }: { children: React.ReactNode }) { - await requireUser(); + const u = await requireUser(); return
- +
{children}
; } diff --git a/app/app/page.tsx b/app/app/page.tsx index 4e8d340..00007f1 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -1 +1,2 @@ -export default function NewAnalysis() { return

New analysis

; } +import { Runner } from "@/components/runner"; +export default function Run() { return ; } diff --git a/components/export-buttons.tsx b/components/export-buttons.tsx new file mode 100644 index 0000000..c0949f7 --- /dev/null +++ b/components/export-buttons.tsx @@ -0,0 +1,11 @@ +"use client"; +export function ExportButtons({ oul, title }: { oul: string; title: string }) { + const copy = async () => { + const sql = [...document.querySelectorAll("#report pre.sql")].map((p) => p.textContent).join("\n\n"); + await navigator.clipboard.writeText(`${title}\n\n${sql || oul}`); + }; + return
+ + +
; +} diff --git a/components/history-item.tsx b/components/history-item.tsx new file mode 100644 index 0000000..e24fc91 --- /dev/null +++ b/components/history-item.tsx @@ -0,0 +1,30 @@ +"use client"; +import { rename, remove } from "@/lib/actions"; + +export function HistoryItem({ row }: { row: { id: string; title: string; status: string; created_at: number } }) { + // The server actions take (previousState, formData) for useActionState; a plain
passes only + // formData, so bind the unused slot here rather than duplicating two single-arg actions. + const doRename = (fd: FormData) => rename(null, fd); + const doRemove = (fd: FormData) => remove(null, fd); + const when = new Date(row.created_at).toLocaleString(undefined, + { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); + return
  • + + {row.title}{row.status !== "done" && {row.status}} + +
    + {when} + + + + + + {/* Delete is the only destructive action in the product: it never fires on a stray click. */} +
    { if (!confirm("Delete this analysis?")) e.preventDefault(); }}> + + +
    +
    +
  • ; +} diff --git a/components/rail.tsx b/components/rail.tsx new file mode 100644 index 0000000..93606ba --- /dev/null +++ b/components/rail.tsx @@ -0,0 +1,15 @@ +import { listAnalyses } from "@/lib/db"; +import { HistoryItem } from "./history-item"; + +export function Rail({ userId }: { userId: string }) { + const rows = listAnalyses(userId); + return <> + qAI +

    History · {rows.length}

    + +

    + Settings

    + ; +} diff --git a/components/report-view.tsx b/components/report-view.tsx new file mode 100644 index 0000000..97a8eaf --- /dev/null +++ b/components/report-view.tsx @@ -0,0 +1,8 @@ +"use client"; +import { Renderer } from "@openuidev/react-lang"; +import { library } from "@/lib/report-ui"; + +export function ReportView({ oul, streaming }: { oul: string; streaming: boolean }) { + if (!oul) return null; + return ; +} diff --git a/components/runner.tsx b/components/runner.tsx new file mode 100644 index 0000000..8a462f3 --- /dev/null +++ b/components/runner.tsx @@ -0,0 +1,74 @@ +"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 = { 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(null); + const [oul, setOul] = useState(""); const [error, setError] = useState(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
    +

    Paste a plan

    +
    + + Where do I get this XML? +
      +
    1. SSMS: press Ctrl+M (Include Actual Execution Plan), then run the query.
    2. +
    3. On the Execution Plan tab, right-click → Save As .sqlplan. Or right-click the statement → Showplan XML and copy the cell.
    4. +
    5. No SSMS: prefix the batch with SET STATISTICS XML ON; and copy the XML result cell.
    6. +
    7. Drop the file or paste its contents below. Only the digest leaves this container, and only to your own models.
    8. +
    +
    +
    + +