feat: console ui, streaming report, history actions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
ee4a88e499
commit
a5529ce542
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||||
|
|
||||||
|
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||||
|
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
@@ -45,7 +45,10 @@ export const db: DatabaseSync = new Proxy({} as DatabaseSync, {
|
|||||||
|
|
||||||
const now = () => Date.now();
|
const now = () => Date.now();
|
||||||
type Row = string | number | null;
|
type Row = string | number | null;
|
||||||
const one = <T>(sql: string, ...p: Row[]): T | null => (db.prepare(sql).get(...p) as T | undefined) ?? null;
|
// node:sqlite hands back null-prototype objects. React refuses to serialize those into a client component
|
||||||
|
// ("Only plain objects ... can be passed to Client Components"), so every row is spread back to a normal object here.
|
||||||
|
const plain = <T>(r: unknown): T | null => (r ? { ...r } as T : null);
|
||||||
|
const one = <T>(sql: string, ...p: Row[]): T | null => plain<T>(db.prepare(sql).get(...p));
|
||||||
// node:sqlite binds null/number/string/bigint/Buffer only: pass 0/1, never booleans.
|
// node:sqlite binds null/number/string/bigint/Buffer only: pass 0/1, never booleans.
|
||||||
|
|
||||||
export type SettingsInput = { analyst_base_url: string; analyst_key: string; analyst_model: string;
|
export type SettingsInput = { analyst_base_url: string; analyst_key: string; analyst_model: string;
|
||||||
@@ -106,8 +109,9 @@ export function patchAnalysis(id: string, p: Record<string, string>) {
|
|||||||
.run(...keys.map((k) => p[k]), now(), id);
|
.run(...keys.map((k) => p[k]), now(), id);
|
||||||
}
|
}
|
||||||
export const listAnalyses = (userId: string) =>
|
export const listAnalyses = (userId: string) =>
|
||||||
db.prepare("SELECT id, title, status, created_at FROM analyses WHERE user_id = ? ORDER BY created_at DESC")
|
(db.prepare("SELECT id, title, status, created_at FROM analyses WHERE user_id = ? ORDER BY created_at DESC")
|
||||||
.all(userId) as { id: string; title: string; status: string; created_at: number }[];
|
.all(userId) as { id: string; title: string; status: string; created_at: number }[])
|
||||||
|
.map((r) => ({ ...r }));
|
||||||
export const getAnalysis = (id: string, userId: string) =>
|
export const getAnalysis = (id: string, userId: string) =>
|
||||||
one<Analysis>("SELECT * FROM analyses WHERE id = ? AND user_id = ?", id, userId);
|
one<Analysis>("SELECT * FROM analyses WHERE id = ? AND user_id = ?", id, userId);
|
||||||
export const renameAnalysis = (id: string, userId: string, title: string) =>
|
export const renameAnalysis = (id: string, userId: string, title: string) =>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { chromium } from "@playwright/test";
|
||||||
|
const ID = process.argv[2] ?? "660dd007-cc6f-4644-a57b-9c32a012e60a";;
|
||||||
|
const b = await chromium.launch();
|
||||||
|
const ctx = await b.newContext();
|
||||||
|
await ctx.addCookies([{ name: "qai_s", value: "tokAPI", url: "http://localhost:3117" }]);
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
const errs = [];
|
||||||
|
page.on("pageerror", (e) => errs.push(String(e.message).slice(0, 140)));
|
||||||
|
await page.goto(`http://localhost:3117/app/${ID}`, { waitUntil: "domcontentloaded" });
|
||||||
|
await page.waitForSelector("#report", { timeout: 25000 });
|
||||||
|
const q = async (sel) => page.locator(sel).count();
|
||||||
|
console.log("QAI report", await q("#report"), "cards", await q("#report .card"), "sql blocks", await q("#report pre.sql"),
|
||||||
|
"copy buttons", await q("#report button"), "tables", await q("#report table"), "bars", await q("#report .bar"),
|
||||||
|
"verdict", await q("[data-verdict]"), "metrics", await q(".grid-metrics .card"));
|
||||||
|
console.log("QAI headline", (await page.locator("#report h2").first().textContent())?.slice(0, 90));
|
||||||
|
console.log("QAI console errors", errs.length, errs.slice(0, 2));
|
||||||
|
for (const w of [320, 375, 414, 768]) {
|
||||||
|
await page.setViewportSize({ width: w, height: 800 });
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const over = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||||
|
console.log(`QAI ${w}px overflow ${over}`);
|
||||||
|
}
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
await page.emulateMedia({ media: "print" });
|
||||||
|
console.log("QAI print rail hidden", await page.locator(".rail").isHidden(), "toolbar hidden", await page.locator(".toolbar").first().isHidden(),
|
||||||
|
"body bg", await page.evaluate(() => getComputedStyle(document.body).backgroundColor));
|
||||||
|
await b.close();
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { chromium } from "@playwright/test";
|
||||||
|
const ID = process.argv[2];
|
||||||
|
const PORT = process.argv[3] ?? "3117";
|
||||||
|
const b = await chromium.launch();
|
||||||
|
const ctx = await b.newContext();
|
||||||
|
await ctx.addCookies([{ name: "qai_s", value: "tokAPI", url: `http://localhost:${PORT}` }]);
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
page.on("console", (m) => { if (m.type() === "error") console.log("QAI console", m.text().slice(0, 400)); });
|
||||||
|
page.on("pageerror", (e) => console.log("QAI pageerror", String(e).slice(0, 500)));
|
||||||
|
await page.goto(`http://localhost:${PORT}/app/${ID}`, { waitUntil: "load" });
|
||||||
|
await page.waitForTimeout(6000);
|
||||||
|
console.log("QAI report count", await page.locator("#report").count());
|
||||||
|
await b.close();
|
||||||
@@ -37,3 +37,14 @@ test("session resolves only while unexpired", () => {
|
|||||||
q.dropSession("tok");
|
q.dropSession("tok");
|
||||||
assert.equal(q.sessionUserId("tok"), null);
|
assert.equal(q.sessionUserId("tok"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rows are plain objects so they can cross the server -> client boundary", () => {
|
||||||
|
const id = q.createUser("plain@x.y", "h");
|
||||||
|
const aid = q.newAnalysis(id, "t", "<xml/>");
|
||||||
|
const row = q.listAnalyses(id)[0];
|
||||||
|
assert.equal(Object.getPrototypeOf(row), Object.prototype, "listAnalyses returned a null-prototype row");
|
||||||
|
assert.equal(Object.getPrototypeOf(q.getAnalysis(aid, id)!), Object.prototype, "getAnalysis");
|
||||||
|
q.saveSettings(id, { analyst_base_url: "b", analyst_key: "k", analyst_model: "m", analyst_extra: "{}",
|
||||||
|
jev_key: "j", jev_model: "v", onboarded: 1 });
|
||||||
|
assert.equal(Object.getPrototypeOf(q.getSettings(id)!), Object.prototype, "getSettings");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user