Compare commits
8 changed files with 1 additions and 578 deletions
|
|
@ -16,7 +16,6 @@ domain, covers host-level detail for one hive.
|
||||||
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
||||||
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
||||||
| `/issues` | a cross-repo issue report |
|
| `/issues` | a cross-repo issue report |
|
||||||
| `/review-tryout` | try a review classification against a real PR |
|
|
||||||
|
|
||||||
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
||||||
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
||||||
|
|
@ -47,27 +46,6 @@ page. Selecting outside it, or Escape, closes it. You can stop or pause the
|
||||||
agent, or link an account, without leaving the terminal. A subagent's tab opens
|
agent, or link an account, without leaving the terminal. A subagent's tab opens
|
||||||
its agent's details.
|
its agent's details.
|
||||||
|
|
||||||
### Review try-out
|
|
||||||
|
|
||||||
`/review-tryout` shows how the [review classifier](review-classifier.md)
|
|
||||||
answers a classification on a real PR, before you put it in a repo's config.
|
|
||||||
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.
|
|
||||||
|
|
||||||
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
|
|
||||||
reason. A classification that couldn't run shows its error where the reason
|
|
||||||
would be. Under the table are the overall verdict, approve or request changes,
|
|
||||||
and the head commit the run read. A failed run shows its error, for example when the swarm
|
|
||||||
has no API key yet.
|
|
||||||
|
|
||||||
What you typed stays after a run, so you can edit the classification and run it
|
|
||||||
again. The page saves no classification and no result, and posts nothing to the
|
|
||||||
forge; like every table in the UI, the result table remembers its column
|
|
||||||
filters in the browser. It shows no token counts, cache counts or cost.
|
|
||||||
|
|
||||||
### Scheduled prompts
|
### Scheduled prompts
|
||||||
|
|
||||||
`/schedules` has one card per scheduled prompt. The **name** is the card's
|
`/schedules` has one card per scheduled prompt. The **name** is the card's
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { ComponentsPage } from "./pages/ComponentsPage.js";
|
||||||
import { JobsPage } from "./pages/JobsPage.js";
|
import { JobsPage } from "./pages/JobsPage.js";
|
||||||
import { HivesPage } from "./pages/HivesPage.js";
|
import { HivesPage } from "./pages/HivesPage.js";
|
||||||
import { IssueReportPage } from "./pages/IssueReportPage.js";
|
import { IssueReportPage } from "./pages/IssueReportPage.js";
|
||||||
import { ReviewTryOutPage } from "./pages/review-tryout/ReviewTryOutPage.js";
|
|
||||||
import { SchedulesPage } from "./pages/schedules/SchedulesPage.js";
|
import { SchedulesPage } from "./pages/schedules/SchedulesPage.js";
|
||||||
import { Panel } from "./ui/panel/Panel.js";
|
import { Panel } from "./ui/panel/Panel.js";
|
||||||
|
|
||||||
|
|
@ -34,7 +33,6 @@ export function App() {
|
||||||
<Route path="/jobs" component={JobsPage} />
|
<Route path="/jobs" component={JobsPage} />
|
||||||
<Route path="/schedules" component={SchedulesPage} />
|
<Route path="/schedules" component={SchedulesPage} />
|
||||||
<Route path="/issues" component={IssueReportPage} />
|
<Route path="/issues" component={IssueReportPage} />
|
||||||
<Route path="/review-tryout" component={ReviewTryOutPage} />
|
|
||||||
<Route path="/components" component={ComponentsPage} />
|
<Route path="/components" component={ComponentsPage} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// <ClassificationEditor> — the fields of one review classification: a name, a
|
|
||||||
// description (the prompt the model answers), which answer passes, and how
|
|
||||||
// certain the model has to be for its answer to count.
|
|
||||||
import { Button } from "../../ui/button/Button.js";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const POLARITY_OPTIONS = [
|
|
||||||
{ value: "positive", label: "positive — the PR should match" },
|
|
||||||
{ value: "negative", label: "negative — the PR should not match" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ClassificationEditor({
|
|
||||||
draft,
|
|
||||||
onChange,
|
|
||||||
onRemove,
|
|
||||||
}: {
|
|
||||||
draft: ClassificationDraft;
|
|
||||||
onChange: (next: ClassificationDraft) => void;
|
|
||||||
/** Absent when this is the only classification: one is the minimum. */
|
|
||||||
onRemove?: () => void;
|
|
||||||
}) {
|
|
||||||
const id = `tryout-${draft.key}`;
|
|
||||||
return (
|
|
||||||
<div class="ui-tryout-classification">
|
|
||||||
<TextField
|
|
||||||
id={`${id}-name`}
|
|
||||||
label="name"
|
|
||||||
value={draft.name}
|
|
||||||
placeholder="e.g. datetime"
|
|
||||||
onInput={(name) => onChange({ ...draft, name })}
|
|
||||||
/>
|
|
||||||
<TextAreaField
|
|
||||||
id={`${id}-description`}
|
|
||||||
label="description"
|
|
||||||
value={draft.description}
|
|
||||||
rows={4}
|
|
||||||
placeholder="what to look for in the PR, e.g. does it add a naive datetime?"
|
|
||||||
onInput={(description) => onChange({ ...draft, description })}
|
|
||||||
/>
|
|
||||||
<div class="ui-tryout-classification-row">
|
|
||||||
<SelectField
|
|
||||||
id={`${id}-polarity`}
|
|
||||||
label="polarity"
|
|
||||||
value={draft.polarity}
|
|
||||||
options={POLARITY_OPTIONS}
|
|
||||||
onChange={(polarity) =>
|
|
||||||
onChange({ ...draft, polarity: polarity as Polarity })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id={`${id}-threshold`}
|
|
||||||
label="threshold (0–100)"
|
|
||||||
value={draft.threshold}
|
|
||||||
pattern="[0-9]+"
|
|
||||||
onInput={(threshold) => onChange({ ...draft, threshold })}
|
|
||||||
/>
|
|
||||||
{onRemove ? <Button onClick={onRemove}>remove</Button> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/* The form panel and the result panel under it, apart from each other. */
|
|
||||||
.ui-tryout-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
/* The try-out form: the repo and PR on one line, then one block per
|
|
||||||
classification, then the actions. */
|
|
||||||
.ui-tryout-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
.ui-tryout-target {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.75em;
|
|
||||||
}
|
|
||||||
.ui-tryout-target > * {
|
|
||||||
flex: 1 1 14em;
|
|
||||||
}
|
|
||||||
.ui-tryout-classification {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.6em;
|
|
||||||
padding: 0.75em;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
.ui-tryout-classification-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 0.75em;
|
|
||||||
}
|
|
||||||
.ui-tryout-classification-row > * {
|
|
||||||
flex: 1 1 12em;
|
|
||||||
}
|
|
||||||
.ui-tryout-classification-row > button {
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
.ui-tryout-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75em;
|
|
||||||
}
|
|
||||||
.ui-tryout-hint {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.ui-tryout-running {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.ui-tryout-row-error {
|
|
||||||
color: var(--red);
|
|
||||||
}
|
|
||||||
.ui-tryout-overall,
|
|
||||||
.ui-tryout-sha {
|
|
||||||
margin: 0.75em 0 0;
|
|
||||||
}
|
|
||||||
.ui-tryout-sha {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
@ -1,239 +0,0 @@
|
||||||
// <ReviewTryOutPage> — 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
|
|
||||||
// swarm-controller's `POST /api/review-classifier/try-out`, which answers
|
|
||||||
// `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.
|
|
||||||
//
|
|
||||||
// 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
|
|
||||||
// column filters, as for every table in the UI.
|
|
||||||
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 { TextField } from "../../ui/text-field/TextField.js";
|
|
||||||
import { ClassificationEditor } from "./ClassificationEditor.js";
|
|
||||||
import { TryOutResult } from "./TryOutResult.js";
|
|
||||||
import {
|
|
||||||
draftProblem,
|
|
||||||
duplicateName,
|
|
||||||
prNumber,
|
|
||||||
repoProblem,
|
|
||||||
toBody,
|
|
||||||
type ClassificationDraft,
|
|
||||||
type TryOutResult as Result,
|
|
||||||
} from "./TryOutTypes.js";
|
|
||||||
import "./ReviewTryOutPage.css";
|
|
||||||
|
|
||||||
const TRY_OUT = "/api/review-classifier/try-out";
|
|
||||||
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
|
|
||||||
// shows as it is.
|
|
||||||
const MAX_CLASSIFICATIONS = 10;
|
|
||||||
|
|
||||||
let nextKey = 0;
|
|
||||||
function blankDraft(): ClassificationDraft {
|
|
||||||
return {
|
|
||||||
key: nextKey++,
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
polarity: "positive",
|
|
||||||
threshold: "80",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReviewTryOutPage() {
|
|
||||||
const [repo, setRepo] = useState("");
|
|
||||||
const [pr, setPr] = useState("");
|
|
||||||
const [drafts, setDrafts] = useState<ClassificationDraft[]>(() => [
|
|
||||||
blankDraft(),
|
|
||||||
]);
|
|
||||||
// The id of the run being polled; null when none is in flight.
|
|
||||||
const [runId, setRunId] = useState<string | null>(null);
|
|
||||||
const [starting, setStarting] = useState(false);
|
|
||||||
const [result, setResult] = useState<Result | null>(null);
|
|
||||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
|
||||||
// Whole seconds since the run started, so a long wait shows it is alive.
|
|
||||||
const [elapsed, setElapsed] = useState(0);
|
|
||||||
|
|
||||||
const running = starting || runId !== null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!running) return;
|
|
||||||
setElapsed(0);
|
|
||||||
const t = setInterval(() => setElapsed((s) => s + 1), 1000);
|
|
||||||
return () => clearInterval(t);
|
|
||||||
}, [running]);
|
|
||||||
|
|
||||||
// Polls the run until it leaves `running`. Cancelled by the next run, or by
|
|
||||||
// leaving the page.
|
|
||||||
useEffect(() => {
|
|
||||||
if (runId === null) return;
|
|
||||||
let cancelled = false;
|
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
const stop = (problem: ProblemDetails) => {
|
|
||||||
setError(problem);
|
|
||||||
setRunId(null);
|
|
||||||
};
|
|
||||||
async function poll() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`${TRY_OUT}/${encodeURIComponent(runId ?? "")}`,
|
|
||||||
);
|
|
||||||
if (cancelled) return;
|
|
||||||
if (res.status === 404) {
|
|
||||||
stop({
|
|
||||||
detail:
|
|
||||||
"the result is gone, because it expired or the controller restarted: run it again",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!res.ok) {
|
|
||||||
const problem = await readApiError(res);
|
|
||||||
if (!cancelled) stop(problem);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const body = (await res.json()) as Result;
|
|
||||||
if (cancelled) return;
|
|
||||||
setResult(body);
|
|
||||||
if (body.state === "running")
|
|
||||||
timer = setTimeout(() => void poll(), POLL_MS);
|
|
||||||
else setRunId(null);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if (!cancelled) stop({ detail: String(e) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void poll();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
if (timer !== undefined) clearTimeout(timer);
|
|
||||||
};
|
|
||||||
}, [runId]);
|
|
||||||
|
|
||||||
const problem =
|
|
||||||
repoProblem(repo) ??
|
|
||||||
(prNumber(pr) === null ? "a PR number is a whole number above 0" : null) ??
|
|
||||||
drafts.map(draftProblem).find((p) => p !== null) ??
|
|
||||||
duplicateName(drafts) ??
|
|
||||||
null;
|
|
||||||
|
|
||||||
async function run(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
const number = prNumber(pr);
|
|
||||||
if (problem !== null || number === null) return;
|
|
||||||
setStarting(true);
|
|
||||||
setError(null);
|
|
||||||
setResult(null);
|
|
||||||
try {
|
|
||||||
const res = await fetch(TRY_OUT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
repo: repo.trim(),
|
|
||||||
pr: number,
|
|
||||||
classifications: drafts.map(toBody),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
setError(await readApiError(res));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setRunId(((await res.json()) as { id: string }).id);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setError({ detail: String(err) });
|
|
||||||
} finally {
|
|
||||||
setStarting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeDraft(next: ClassificationDraft) {
|
|
||||||
setDrafts((ds) => ds.map((d) => (d.key === next.key ? next : d)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="ui-tryout-page">
|
|
||||||
<Panel title="review try-out" icon="🧪">
|
|
||||||
<p>
|
|
||||||
Run a review classification against a real PR and see how the model
|
|
||||||
answers it. Nothing is posted to the forge, and nothing is saved:
|
|
||||||
change the classification and run it again until it answers the way
|
|
||||||
you want.
|
|
||||||
</p>
|
|
||||||
<form class="ui-tryout-form" onSubmit={(e) => void run(e)}>
|
|
||||||
<div class="ui-tryout-target">
|
|
||||||
<TextField
|
|
||||||
id="tryout-repo"
|
|
||||||
label="repo"
|
|
||||||
value={repo}
|
|
||||||
placeholder="owner/name"
|
|
||||||
onInput={setRepo}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="tryout-pr"
|
|
||||||
label="PR number"
|
|
||||||
value={pr}
|
|
||||||
pattern="[0-9]+"
|
|
||||||
onInput={setPr}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{drafts.map((d) => (
|
|
||||||
<ClassificationEditor
|
|
||||||
key={d.key}
|
|
||||||
draft={d}
|
|
||||||
onChange={changeDraft}
|
|
||||||
onRemove={
|
|
||||||
drafts.length > 1
|
|
||||||
? () => setDrafts((ds) => ds.filter((x) => x.key !== d.key))
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<div class="ui-tryout-actions">
|
|
||||||
<Button
|
|
||||||
onClick={() => setDrafts((ds) => [...ds, blankDraft()])}
|
|
||||||
disabled={drafts.length >= MAX_CLASSIFICATIONS}
|
|
||||||
>
|
|
||||||
add a classification
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={running || problem !== null}
|
|
||||||
>
|
|
||||||
{running ? "running…" : "run"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{problem !== null ? <p class="ui-tryout-hint">{problem}</p> : null}
|
|
||||||
</form>
|
|
||||||
</Panel>
|
|
||||||
{running || result || error ? (
|
|
||||||
<Panel title="result" icon="📋">
|
|
||||||
{running ? (
|
|
||||||
<p class="ui-tryout-running" role="status">
|
|
||||||
Running — a run takes a minute or more, and up to half an hour for
|
|
||||||
a long list ({elapsed}s so far).
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{error ? (
|
|
||||||
<ApiErrorPanel context="the try-out failed" problem={error} />
|
|
||||||
) : null}
|
|
||||||
{result?.state === "failed" ? (
|
|
||||||
<ApiErrorPanel
|
|
||||||
context="the run failed"
|
|
||||||
problem={{
|
|
||||||
title: "the classifier could not finish",
|
|
||||||
detail: result.error ?? "no reason given",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{result?.state === "done" ? <TryOutResult result={result} /> : null}
|
|
||||||
</Panel>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
// <TryOutResult> — a finished try-out: one row per classification, the
|
|
||||||
// overall verdict under them, and the head commit the run read. The columns
|
|
||||||
// are the review's own table (`docs/swarm/review-classifier.md`); no token
|
|
||||||
// counts, cache counts or cost, here or anywhere in the UI.
|
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
|
||||||
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
|
||||||
import type { TryOutResult as Result, TryOutRow } from "./TryOutTypes.js";
|
|
||||||
|
|
||||||
const VERDICT_LABEL: Record<TryOutRow["verdict"], string> = {
|
|
||||||
match: "match",
|
|
||||||
"no-match": "no match",
|
|
||||||
"not-applicable": "not applicable",
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLUMNS: TableColumn<TryOutRow>[] = [
|
|
||||||
{ key: "name", header: "classification", render: (r) => r.name },
|
|
||||||
{ key: "polarity", header: "polarity", render: (r) => r.polarity },
|
|
||||||
{
|
|
||||||
key: "verdict",
|
|
||||||
header: "verdict",
|
|
||||||
render: (r) => (r.error ? "—" : VERDICT_LABEL[r.verdict]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "certainty",
|
|
||||||
header: "certainty",
|
|
||||||
render: (r) => (r.error ? "—" : r.certainty),
|
|
||||||
},
|
|
||||||
{ key: "threshold", header: "threshold", render: (r) => r.threshold },
|
|
||||||
{
|
|
||||||
key: "counted",
|
|
||||||
header: "counted",
|
|
||||||
// A counted row shows whether it passed its polarity; one below its
|
|
||||||
// threshold does not count and has no pass or fail.
|
|
||||||
render: (r) =>
|
|
||||||
r.error ? "—" : r.counted ? (r.passed ? "yes ✅" : "yes ❌") : "no",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "reason",
|
|
||||||
header: "reason",
|
|
||||||
cellClass: "ui-table-prose",
|
|
||||||
// A row that could not run says why where the reason would be.
|
|
||||||
render: (r) =>
|
|
||||||
r.error ? <span class="ui-tryout-row-error">{r.error}</span> : r.reason,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function TryOutResult({ result }: { result: Result }) {
|
|
||||||
const rows = result.rows ?? [];
|
|
||||||
return (
|
|
||||||
<div class="ui-tryout-result">
|
|
||||||
<Table
|
|
||||||
columns={COLUMNS}
|
|
||||||
rows={rows}
|
|
||||||
// By position: two classifications may share a name, and a shared key
|
|
||||||
// would let one result render over the other.
|
|
||||||
rowKey={(r) => String(rows.indexOf(r))}
|
|
||||||
storageKey="swarm-ui:review-tryout:table-filters"
|
|
||||||
emptyMessage="the run returned no classifications"
|
|
||||||
/>
|
|
||||||
{result.overall ? (
|
|
||||||
<p class="ui-tryout-overall">
|
|
||||||
<strong>Overall: </strong>
|
|
||||||
<Badge
|
|
||||||
tone={result.overall === "approve" ? "positive" : "negative"}
|
|
||||||
value={result.overall === "approve" ? "approve" : "request changes"}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
<p class="ui-tryout-sha">
|
|
||||||
Ran on head <code>{result.head_sha.slice(0, 12)}</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
// Wire shapes of the review classifier's try-out endpoints
|
|
||||||
// (`POST /api/review-classifier/try-out`, `GET .../try-out/{id}`), and the
|
|
||||||
// checks the form makes before a request is worth sending. The backend stays
|
|
||||||
// the real gate: these only keep an obviously bad form from a round trip.
|
|
||||||
|
|
||||||
export type Polarity = "positive" | "negative";
|
|
||||||
|
|
||||||
/** One classification as the form holds it: every field as the user typed it. */
|
|
||||||
export interface ClassificationDraft {
|
|
||||||
/** Local only, so a row keeps its identity when another is removed. */
|
|
||||||
key: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
polarity: Polarity;
|
|
||||||
threshold: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The request's classification: what the backend is sent. */
|
|
||||||
export interface ClassificationBody {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
polarity: Polarity;
|
|
||||||
threshold: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Verdict = "match" | "no-match" | "not-applicable";
|
|
||||||
|
|
||||||
export interface TryOutRow {
|
|
||||||
name: string;
|
|
||||||
polarity: Polarity;
|
|
||||||
verdict: Verdict;
|
|
||||||
certainty: number;
|
|
||||||
threshold: number;
|
|
||||||
/** Whether the verdict counted: its certainty reached the threshold. */
|
|
||||||
counted: boolean;
|
|
||||||
/** For a counted row, whether it passed its polarity. */
|
|
||||||
passed: boolean;
|
|
||||||
reason: string;
|
|
||||||
/** Set when this classification could not be run. */
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TryOutOverall = "approve" | "request-changes";
|
|
||||||
|
|
||||||
export interface TryOutResult {
|
|
||||||
state: "running" | "done" | "failed";
|
|
||||||
head_sha: string;
|
|
||||||
rows?: TryOutRow[];
|
|
||||||
overall?: TryOutOverall;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Characters the endpoint accepts in each half of a repo: letters, digits, `.`,
|
|
||||||
// `_` and `-`, other than `.` and `..` themselves.
|
|
||||||
const SEGMENT = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
|
|
||||||
|
|
||||||
export function repoProblem(repo: string): string | null {
|
|
||||||
const parts = repo.trim().split("/");
|
|
||||||
return parts.length === 2 && parts.every((p) => SEGMENT.test(p))
|
|
||||||
? null
|
|
||||||
: "a repo is owner/name, with letters, digits, . _ and - in each part";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prNumber(pr: string): number | null {
|
|
||||||
const n = Number(pr.trim());
|
|
||||||
return /^\d+$/.test(pr.trim()) && n > 0 ? n : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The threshold as a number, or null when it is not a whole 0–100. */
|
|
||||||
export function thresholdValue(text: string): number | null {
|
|
||||||
const t = text.trim();
|
|
||||||
if (!/^\d+$/.test(t)) return null;
|
|
||||||
const n = Number(t);
|
|
||||||
return n <= 100 ? n : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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";
|
|
||||||
if (d.description.trim() === "") {
|
|
||||||
return `${d.name.trim()} needs a description`;
|
|
||||||
}
|
|
||||||
if (thresholdValue(d.threshold) === null) {
|
|
||||||
return `${d.name.trim()}: the threshold is a whole number from 0 to 100`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A name two classifications share, as a problem, or null: the result table
|
|
||||||
* names its rows by it, so a shared name leaves two rows nobody can tell apart. */
|
|
||||||
export function duplicateName(drafts: ClassificationDraft[]): string | null {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const d of drafts) {
|
|
||||||
const name = d.name.trim();
|
|
||||||
if (seen.has(name)) return `two classifications are named ${name}`;
|
|
||||||
seen.add(name);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toBody(d: ClassificationDraft): ClassificationBody {
|
|
||||||
return {
|
|
||||||
name: d.name.trim(),
|
|
||||||
description: d.description,
|
|
||||||
polarity: d.polarity,
|
|
||||||
threshold: thresholdValue(d.threshold) ?? 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -51,7 +51,6 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
||||||
{ href: "/jobs", label: "jobs", accent: "var(--pink)" },
|
{ href: "/jobs", label: "jobs", accent: "var(--pink)" },
|
||||||
{ href: "/schedules", label: "schedules", accent: "var(--cyan)" },
|
{ href: "/schedules", label: "schedules", accent: "var(--cyan)" },
|
||||||
{ href: "/issues", label: "issues", accent: "var(--yellow)" },
|
{ href: "/issues", label: "issues", accent: "var(--yellow)" },
|
||||||
{ href: "/review-tryout", label: "review try-out", accent: "var(--red)" },
|
|
||||||
{ href: "/components", label: "components", accent: "var(--blue)" },
|
{ href: "/components", label: "components", accent: "var(--blue)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -62,7 +61,7 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
||||||
// modifier alongside the base class, not a replacement, so every other
|
// modifier alongside the base class, not a replacement, so every other
|
||||||
// route's layout is untouched. `/agents` joined for the same reason —
|
// route's layout is untouched. `/agents` joined for the same reason —
|
||||||
// its own table view (plus the list+detail split) hits the same cap.
|
// its own table view (plus the list+detail split) hits the same cap.
|
||||||
const WIDE_BODY_ROUTES = new Set(["/issues", "/agents", "/review-tryout"]);
|
const WIDE_BODY_ROUTES = new Set(["/issues", "/agents"]);
|
||||||
|
|
||||||
// `/agents/:name/terminal` (and a subagent's, under
|
// `/agents/:name/terminal` (and a subagent's, under
|
||||||
// `/agents/:name/subagents/:subagent/`) is full-bleed: the terminal fills
|
// `/agents/:name/subagents/:subagent/`) is full-bleed: the terminal fills
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue