Files
byre/web/dist/reviews.js
T

133 lines
5.7 KiB
JavaScript
Raw Normal View History

import { get } from "./api.js";
import { ago, errorText, h, replace } from "./dom.js";
import { markdown } from "./markdown.js";
import { onEach, pickReview, state } from "./state.js";
const severities = ["critical", "high", "medium", "low", "info"];
export function isReviewEvent(ev) {
return ev.kind.startsWith("review.");
}
export function reviewCounts(r) {
const res = (r.result ?? {});
const findings = Array.isArray(res["findings"]) ? res["findings"] : [];
const parts = severities.map((s) => [s, findings.filter((f) => f["severity"] === s).length]).filter(([, n]) => n > 0);
return parts.length > 0 ? parts.map(([s, n]) => `${n} ${s}`).join(", ") : "no findings";
}
function stateText(r) {
switch (r.state) {
case "running":
return `reviewing on ${r.vendor} (${r.model})…`;
case "done":
return reviewCounts(r);
case "failed":
return "failed";
}
}
export function reviewCard(r, taskId) {
return h("article", { class: `review ${r.state}` }, h("header", {}, h("span", { class: "review-snapshot" }, `snapshot #${r.snapshot_n}`), h("span", { class: `review-state ${r.state}` }, stateText(r)), h("time", { dateTime: r.created_at, title: r.created_at }, ago(r.created_at))), r.state === "failed" ? h("p", { class: "error-text" }, r.error) : null, r.state === "done" ? reviewResult(r, taskId) : null);
}
function reviewResult(r, taskId) {
const res = (r.result ?? {});
const findings = Array.isArray(res["findings"]) ? res["findings"] : [];
const md = { taskId };
return h("div", { class: "review-result" }, markdown(String(res["summary"] ?? ""), "md review-summary", md), findings.length > 0
? h("ol", { class: "findings" }, ...findings.map((f) => {
const severity = String(f["severity"] ?? "");
const where = typeof f["path"] === "string" && f["path"] ? `${f["path"]}${typeof f["line"] === "number" && f["line"] > 0 ? `:${f["line"]}` : ""}` : "";
return h("li", { class: `finding ${severity}` }, h("div", { class: "finding-head" }, h("span", { class: `severity ${severity}` }, severity), typeof f["category"] === "string" && f["category"] ? h("span", { class: "finding-category" }, f["category"]) : null, h("span", { class: "finding-title" }, String(f["title"] ?? "")), where ? h("code", { class: "finding-where" }, where) : null), markdown(String(f["detail"] ?? ""), "md finding-detail", md));
}))
: null);
}
export function mountReviews(root) {
const head = h("header", { class: "reviews-header" });
const body = h("div", { class: "review-list" });
let reviews;
let error = "";
let target = 0;
let disposed = false;
const load = async () => {
const taskId = state.selectedTaskId;
if (!taskId) {
render();
return;
}
const started = ++target;
try {
const fresh = await get("/api/tasks/{taskId}/reviews", { path: { taskId } });
if (started === target) {
reviews = fresh;
error = "";
}
}
catch (err) {
if (started === target) {
reviews = undefined;
error = errorText(err);
}
}
if (!disposed && started === target) {
render();
}
};
const render = () => {
const taskId = state.selectedTaskId;
replace(head);
head.hidden = true;
if (!taskId) {
replace(body, h("div", { class: "empty" }, "No task selected."));
return;
}
if (error) {
replace(body, h("div", { class: "empty error-text" }, error));
return;
}
if (!reviews) {
replace(body, h("div", { class: "empty" }, "Loading…"));
return;
}
if (reviews.length === 0) {
replace(body, h("div", { class: "empty" }, "No reviews yet. Each new snapshot of the task is reviewed by an agent from the other vendor."));
return;
}
const n = state.reviewed ?? reviews.at(-1).snapshot_n;
const shown = reviews.findLast((r) => r.snapshot_n === n);
const ns = [...new Set([n, ...reviews.map((r) => r.snapshot_n)])].sort((a, b) => b - a);
const picker = h("select", { title: "Snapshot whose review to show" }, ...ns.map((s) => h("option", { value: String(s) }, `snapshot #${s}`)));
picker.value = String(n);
picker.onchange = () => pickReview(Number(picker.value));
const step = (to, label) => {
const b = h("button", { type: "button", class: "snap-step", disabled: !ns.includes(to) }, label);
b.onclick = () => pickReview(to);
return b;
};
const next = ns.filter((s) => s > n).at(-1);
const prev = ns.find((s) => s < n);
head.hidden = false;
replace(head, picker, step(prev ?? n - 1, `← #${prev ?? n - 1}`), step(next ?? n + 1, `#${next ?? n + 1} →`));
replace(body, shown ? reviewCard(shown, taskId) : h("div", { class: "empty" }, `No review of snapshot #${n} yet.`));
};
const offs = [
onEach(["selection"], () => {
target++;
reviews = undefined;
error = "";
render();
void load();
}),
onEach(["transcript"], (fresh) => {
if (fresh.some(isReviewEvent)) {
void load();
}
}),
onEach(["review"], render),
];
replace(root, h("div", { class: "reviews-view" }, head, body));
render();
void load();
return () => {
disposed = true;
for (const off of offs) {
off();
}
};
}
//# sourceMappingURL=reviews.js.map