Watch
0
0
Fork
You've already forked hyperhive
0

swarm ui: one review page

The two review tabs, runs and try-out, are one: /review. It is a column of
three sections: try a classification and the classifier settings, each a
folded bar (the settings' bar says what they are now), then the recent runs,
always open. The settings had been pinned under the try-out form; they are now
a section of their own. A new Disclosure primitive is the folding bar, a
native details wearing the Panel's classes, so the folded and open sections
read as the same surface.

/review-runs and /review-tryout are gone, with no redirect.
This commit is contained in:
iris 2026-10-14 21:57:23 +02:00 • committed by mara
commit a68d73fa74
19 changed files with 223 additions and 45 deletions

View file

@ -1,8 +1,8 @@
// <ReviewRunsPage> — the review classifier's recent runs, newest first
// <ReviewRuns> — the review classifier's recent runs, newest first
// (`docs/swarm/review-classifier.md`, "Recent runs"): which PRs it ran on,
// what it answered, and which runs failed or were turned away. One card per
// run; selecting a card expands it to the same result table the try-out page
// shows. Reads `GET /api/review-classifier/runs`. The page stores nothing and
// run; selecting a card expands it to the same result table the try-out
// shows. Reads `GET /api/review-classifier/runs`. This section stores nothing and
// posts nothing, and shows no token counts, cache counts or cost.
import { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
@ -25,7 +25,7 @@ import {
durationLabel,
type ClassifierRun,
} from "./RunsTypes.js";
import "./ReviewRunsPage.css";
import "./ReviewRuns.css";
const RUNS = "/api/review-classifier/runs";
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
@ -91,7 +91,7 @@ function RunCard({
);
}
export function ReviewRunsPage() {
export function ReviewRuns() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const [runs, setRuns] = useState<ClassifierRun[] | null>(null);
@ -126,7 +126,7 @@ export function ReviewRunsPage() {
return (
<Panel
title="review runs"
title="recent runs"
icon="📜"
actions={
<RefreshIntervalPicker

View file

@ -10,7 +10,7 @@ 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 { Panel } from "../../ui/panel/Panel.js";
import { Disclosure } from "../../ui/disclosure/Disclosure.js";
import { SelectField } from "../../ui/select-field/SelectField.js";
import { TextField } from "../../ui/text-field/TextField.js";
import {
@ -20,6 +20,7 @@ import {
changed,
draftOf,
settingsProblem,
settingsSummary,
toSettings,
type ClassifierSettingsBody,
type Runtime,
@ -54,6 +55,9 @@ export function ClassifierSettings() {
const [saveError, setSaveError] = useState<ProblemDetails | null>(null);
const [saving, setSaving] = useState(false);
const [justSaved, setJustSaved] = useState(false);
// Folded until wanted: the settings are changed now and then, and the
// folded header says what they are.
const [open, setOpen] = useState(false);
useEffect(() => {
let cancelled = false;
@ -114,7 +118,13 @@ export function ClassifierSettings() {
const problem = draft === null ? null : settingsProblem(draft);
return (
<Panel title="classifier settings" icon="⚙️">
<Disclosure
title="classifier settings"
icon="⚙️"
hint={saved ? settingsSummary(saved) : undefined}
open={open}
onToggle={setOpen}
>
<p>
The runtime, model and effort the review classifier runs with, and how
many requested reviews run at once. The API key runtime runs claude on
@ -234,6 +244,6 @@ export function ClassifierSettings() {
) : null}
</form>
) : null}
</Panel>
</Disclosure>
);
}

View file

@ -182,3 +182,15 @@ export function changed(
d.priceCacheWrite.trim() !== now.priceCacheWrite)
);
}
/** The settings in force as one short line, for a folded view of them: the
* runtime and the model it runs, the effort when one is set, and how many
* reviews run at once. The model is the provider's under ACP. */
export function settingsSummary(s: ClassifierSettingsBody): string {
const model = s.runtime === "acp" ? (s.acp?.model ?? "") : s.model;
const parts = [s.runtime, model, s.effort ? `${s.effort} effort` : ""].filter(
(p) => p !== "",
);
parts.push(`${s.max_concurrent_reviews} at once`);
return parts.join(" · ");
}

View file

@ -6,7 +6,7 @@ import { SelectField } from "../../ui/select-field/SelectField.js";
import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js";
import { TextField } from "../../ui/text-field/TextField.js";
import type { ClassificationDraft, Polarity } from "./TryOutTypes.js";
import "./ReviewTryOutPage.css";
import "./ReviewTryOut.css";
const POLARITY_OPTIONS = [
{ value: "positive", label: "positive — the PR should match" },

View file

@ -1,4 +1,4 @@
// <ReviewTryOutPage> — write a review classification and see how the model
// <ReviewTryOut> — write a review classification and see how the model
// answers it on a real PR, before it goes into a repo's config
// (`docs/swarm/review-classifier.md`, "Try-out"). Pick a repo and a PR, write
// one or more classifications, run: the page posts them to
@ -18,11 +18,8 @@
// rules file, with the operator as commit author, and links to the forge's page
// for opening the PR from that branch.
//
// Under the form, the classifier's own settings: its model, effort and review
// concurrency (`ClassifierSettings`).
//
// The inputs stay as typed after a run, so the operator can edit and run
// again. Apart from the settings, the page keeps nothing you typed and no
// again. The section keeps nothing you typed and no
// result, and a run posts nothing to the forge; the only thing the browser
// remembers is the result table's column filters, as for every table in the UI.
import { useEffect, useState } from "preact/hooks";
@ -30,9 +27,9 @@ 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 { Disclosure } from "../../ui/disclosure/Disclosure.js";
import { Panel } from "../../ui/panel/Panel.js";
import { TextField } from "../../ui/text-field/TextField.js";
import { ClassifierSettings } from "../review-settings/ClassifierSettings.js";
import { ClassificationEditor } from "./ClassificationEditor.js";
import { TryOutResult } from "./TryOutResult.js";
import {
@ -49,7 +46,7 @@ import {
type RulesAnswer,
type TryOutResult as Result,
} from "./TryOutTypes.js";
import "./ReviewTryOutPage.css";
import "./ReviewTryOut.css";
const TRY_OUT = "/api/review-classifier/try-out";
const RULES = "/api/review-classifier/rules";
@ -76,7 +73,7 @@ function blankDraft(): ClassificationDraft {
};
}
export function ReviewTryOutPage() {
export function ReviewTryOut() {
const [repo, setRepo] = useState("");
const [pr, setPr] = useState("");
const [drafts, setDrafts] = useState<ClassificationDraft[]>(() => [
@ -114,6 +111,10 @@ export function ReviewTryOutPage() {
const running = starting || runId !== null;
// Folded until wanted. What was typed survives a fold, and so does a run's
// result, which sits under the form rather than in it.
const [open, setOpen] = useState(false);
// The repos with an open PR, read once when the page opens.
useEffect(() => {
let cancelled = false;
@ -313,7 +314,13 @@ export function ReviewTryOutPage() {
return (
<div class="ui-tryout-page">
<Panel title="review try-out" icon="🧪">
<Disclosure
title="try a classification"
icon="🧪"
hint="run one on a real PR before it goes into a repo's rules"
open={open}
onToggle={setOpen}
>
<p>
Run a review classification against a real PR and see how the model
answers it. A run posts nothing to the forge, and nothing is saved:
@ -426,7 +433,7 @@ export function ReviewTryOutPage() {
</p>
) : null}
</form>
</Panel>
</Disclosure>
<ConfirmDialog
open={confirmLoad}
label="replace the classifications"
@ -462,7 +469,6 @@ export function ReviewTryOutPage() {
{result?.state === "done" ? <TryOutResult result={result} /> : null}
</Panel>
) : null}
<ClassifierSettings />
</div>
);
}

View file

@ -0,0 +1,6 @@
/* The folded bars, then the runs: one column, one gap between the parts. */
.ui-review-page {
display: flex;
flex-direction: column;
gap: 1em;
}

View file

@ -0,0 +1,20 @@
// <ReviewPage> — the review classifier on one page (`docs/swarm/ui.md`,
// "Review"): the way to try a classification on a PR, the classifier's own
// settings, and the runs it has done. The first two fold, since they are
// used now and then and a folded header says what is in them; the runs are
// what the page is for, so they are always in view, under the two bars.
// Each part keeps its own data and requests: this page only lays them out.
import { ClassifierSettings } from "../review-settings/ClassifierSettings.js";
import { ReviewRuns } from "../review-runs/ReviewRuns.js";
import { ReviewTryOut } from "../review-tryout/ReviewTryOut.js";
import "./ReviewPage.css";
export function ReviewPage() {
return (
<div class="ui-review-page">
<ReviewTryOut />
<ClassifierSettings />
<ReviewRuns />
</div>
);
}