diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 34e69c3d..a4f50251 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -55,6 +55,14 @@ Give it a repo as `owner/name` and a PR number, then write a classification: a name, a description, a polarity (positive or negative) and a threshold from 0 to 100. **add a classification** adds more, and **run** starts the run. +**load the repo's rules** fills the classifications from the rules file on the +repo's default branch, so you can run the repo's own rules or start from them +and edit. It reads the file each time, so a rule merged a minute ago shows up. +The page names the file and the commit it read, and asks before it replaces +classifications you have written. If the repo has no rules file, the file +doesn't parse, or it holds more than ten rules, the page shows the reason and +leaves your classifications as they were. + A run takes a minute or more, and up to half an hour for ten classifications. The page says it's running and shows how long it has been, then shows the review's table: for each classification the verdict, the model's certainty, the threshold, whether the verdict counted and the diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css index 1d51e603..3d9b683d 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.css @@ -20,6 +20,14 @@ .ui-tryout-target > * { flex: 1 1 14em; } +.ui-tryout-target > button { + flex: none; + align-self: flex-end; +} +.ui-tryout-loaded { + margin: 0; + color: var(--muted); +} .ui-tryout-classification { display: flex; flex-direction: column; diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx index 8c134dfa..017d21db 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx @@ -6,6 +6,9 @@ // `202 {id}`, and polls `GET .../try-out/{id}` until the run is `done` or // `failed`. A run takes a minute or more, so the page says it is running. // +// **load the repo's rules** fills the rows from the rules on the repo's default +// branch (`GET /api/review-classifier/rules`), to run as they are or to edit. +// // The inputs stay as typed after a run, so the operator can edit and run // again. The page keeps nothing you typed and no result, and posts nothing to // the forge; the only thing the browser remembers is the result table's @@ -14,22 +17,27 @@ import { useEffect, useState } from "preact/hooks"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Button } from "../../ui/button/Button.js"; +import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js"; import { Panel } from "../../ui/panel/Panel.js"; import { TextField } from "../../ui/text-field/TextField.js"; import { ClassificationEditor } from "./ClassificationEditor.js"; import { TryOutResult } from "./TryOutResult.js"; import { + draftFromRule, draftProblem, duplicateName, + hasTypedText, prNumber, repoProblem, toBody, type ClassificationDraft, + type RulesAnswer, type TryOutResult as Result, } from "./TryOutTypes.js"; import "./ReviewTryOutPage.css"; const TRY_OUT = "/api/review-classifier/try-out"; +const RULES = "/api/review-classifier/rules"; const POLL_MS = 2000; // More classifications than this are not offered a row. It is the try-out // endpoint's own cap, which answers a longer list with a 400 that the page @@ -61,6 +69,14 @@ export function ReviewTryOutPage() { // Whole seconds since the run started, so a long wait shows it is alive. const [elapsed, setElapsed] = useState(0); + const [loadingRules, setLoadingRules] = useState(false); + const [rulesError, setRulesError] = useState(null); + // Where the rows came from, until the next load replaces it. + const [loaded, setLoaded] = useState<(RulesAnswer & { repo: string }) | null>( + null, + ); + const [confirmLoad, setConfirmLoad] = useState(false); + const running = starting || runId !== null; useEffect(() => { @@ -151,6 +167,35 @@ export function ReviewTryOutPage() { } } + // Reads the repo's rules from its default branch into the rows. A failed + // load leaves the rows as they were. + async function loadRules() { + const target = repo.trim(); + setConfirmLoad(false); + setLoadingRules(true); + setRulesError(null); + try { + const res = await fetch(`${RULES}?repo=${encodeURIComponent(target)}`); + if (!res.ok) { + setRulesError(await readApiError(res)); + return; + } + const answer = (await res.json()) as RulesAnswer; + setDrafts(answer.classifications.map((c) => draftFromRule(c, nextKey++))); + setLoaded({ ...answer, repo: target }); + } catch (err: unknown) { + setRulesError({ detail: String(err) }); + } finally { + setLoadingRules(false); + } + } + + // Typed rows are the operator's work, so replacing them is asked first. + function askLoadRules() { + if (hasTypedText(drafts)) setConfirmLoad(true); + else void loadRules(); + } + function changeDraft(next: ClassificationDraft) { setDrafts((ds) => ds.map((d) => (d.key === next.key ? next : d))); } @@ -180,7 +225,28 @@ export function ReviewTryOutPage() { pattern="[0-9]+" onInput={setPr} /> + + {rulesError ? ( + + ) : null} + {loaded ? ( +

+ Loaded {loaded.classifications.length} from{" "} + {loaded.path} on {loaded.branch} of{" "} + {loaded.repo}, read at commit{" "} + {loaded.commit.slice(0, 12)}. Edit them below, then + run. +

+ ) : null} {drafts.map((d) => ( {problem}

: null} + setConfirmLoad(false)} + onConfirm={() => void loadRules()} + > +

+ Loading the rules of {repo.trim()} replaces the classifications you + have written below. +

+
{running || result || error ? ( {running ? ( diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts index 1114448f..698f36ac 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts @@ -23,6 +23,17 @@ export interface ClassificationBody { threshold: number; } +/** The rules route's answer (`GET /api/review-classifier/rules?repo=…`): the + * classifications on the repo's default branch, and where they were read. */ +export interface RulesAnswer { + /** The file read: the `.yml` or the `.json` one. */ + path: string; + branch: string; + /** The default branch's head commit the file was read at. */ + commit: string; + classifications: ClassificationBody[]; +} + export type Verdict = "match" | "no-match" | "not-applicable"; export interface TryOutRow { @@ -74,6 +85,28 @@ export function thresholdValue(text: string): number | null { return n <= 100 ? n : null; } +/** A loaded rule as a form row. */ +export function draftFromRule( + rule: ClassificationBody, + key: number, +): ClassificationDraft { + return { + key, + name: rule.name, + description: rule.description, + polarity: rule.polarity, + threshold: String(rule.threshold), + }; +} + +/** Whether any row holds a name or description the user typed, which loading + * the repo's rules would replace. The untouched starting row does not. */ +export function hasTypedText(drafts: ClassificationDraft[]): boolean { + return drafts.some( + (d) => d.name.trim() !== "" || d.description.trim() !== "", + ); +} + /** Why a classification is not ready to send, or null when it is. */ export function draftProblem(d: ClassificationDraft): string | null { if (d.name.trim() === "") return "give each classification a name";