Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: the review try-out page shows and changes the review classifier's settings

A classifier settings panel under the try-out form reads and saves the model,
effort and how many requested reviews run at once through the settings route.
A save replaces all three; a refused one shows the route's reason and leaves
the form as typed. There is no API key field.
This commit is contained in:
iris 2026-10-12 20:04:15 +02:00 • committed by mara
commit baf282131f

View file

@ -0,0 +1,167 @@
// <ClassifierSettings> — the review classifier's model, effort and how many
// requested reviews run at once, shown and saved here
// (`GET`/`PUT /api/review-classifier/settings`). A PUT replaces the stored
// settings whole, so the form sends all three fields. A refused save shows the
// route's own reason and leaves the form as typed. There is no API key field:
// admins set the key in the bao 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 { SelectField } from "../../ui/select-field/SelectField.js";
import { TextField } from "../../ui/text-field/TextField.js";
import {
EFFORTS,
MAX_REVIEWS,
MIN_REVIEWS,
changed,
draftOf,
settingsProblem,
toSettings,
type ClassifierSettingsBody,
type SettingsDraft,
} from "./SettingsTypes.js";
import "./ClassifierSettings.css";
const SETTINGS = "/api/review-classifier/settings";
const EFFORT_OPTIONS = [
{ value: "", label: "claude's default" },
...EFFORTS.map((e) => ({ value: e, label: e })),
];
export function ClassifierSettings() {
// The settings as the controller holds them; null until the first read.
const [saved, setSaved] = useState<ClassifierSettingsBody | null>(null);
const [draft, setDraft] = useState<SettingsDraft | null>(null);
const [loadError, setLoadError] = useState<ProblemDetails | null>(null);
const [saveError, setSaveError] = useState<ProblemDetails | null>(null);
const [saving, setSaving] = useState(false);
const [justSaved, setJustSaved] = useState(false);
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const res = await fetch(SETTINGS);
if (cancelled) return;
if (!res.ok) {
setLoadError(await readApiError(res));
return;
}
const body = (await res.json()) as ClassifierSettingsBody;
if (cancelled) return;
setSaved(body);
setDraft(draftOf(body));
} catch (e: unknown) {
if (!cancelled) setLoadError({ detail: String(e) });
}
})();
return () => {
cancelled = true;
};
}, []);
function edit(next: SettingsDraft) {
setDraft(next);
setJustSaved(false);
}
async function save(e: Event) {
e.preventDefault();
if (draft === null || settingsProblem(draft) !== null) return;
setSaving(true);
setSaveError(null);
setJustSaved(false);
try {
const res = await fetch(SETTINGS, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(toSettings(draft)),
});
if (!res.ok) {
// The form stays as typed, so the operator can fix it and save again.
setSaveError(await readApiError(res));
return;
}
const body = (await res.json()) as ClassifierSettingsBody;
setSaved(body);
setDraft(draftOf(body));
setJustSaved(true);
} catch (err: unknown) {
setSaveError({ detail: String(err) });
} finally {
setSaving(false);
}
}
const problem = draft === null ? null : settingsProblem(draft);
return (
<Panel title="classifier settings" icon="⚙️">
<p>
The model and effort the review classifier runs with, and how many
requested reviews run at once. A change takes effect from the next run,
with no restart. Try-outs don't count toward the limit. The API key is
set in the bao UI, not here.
</p>
{loadError ? (
<ApiErrorPanel
context="the settings could not be loaded"
problem={loadError}
/>
) : null}
{draft ? (
<form class="ui-classifier-settings" onSubmit={(e) => void save(e)}>
<div class="ui-classifier-settings-fields">
<TextField
id="classifier-model"
label="model"
value={draft.model}
placeholder="haiku"
onInput={(model) => edit({ ...draft, model })}
/>
<SelectField
id="classifier-effort"
label="effort"
value={draft.effort}
options={EFFORT_OPTIONS}
onChange={(effort) => edit({ ...draft, effort })}
/>
<TextField
id="classifier-max"
label={`concurrent reviews (${MIN_REVIEWS} to ${MAX_REVIEWS})`}
value={draft.max}
pattern="[0-9]+"
onInput={(max) => edit({ ...draft, max })}
/>
</div>
{saveError ? (
<ApiErrorPanel
context="the settings were not saved"
problem={saveError}
/>
) : null}
<div class="ui-classifier-settings-actions">
<Button
variant="primary"
type="submit"
disabled={saving || problem !== null || !changed(draft, saved)}
>
{saving ? "saving…" : "save"}
</Button>
{justSaved ? (
<span class="ui-classifier-settings-note" role="status">
Saved.
</span>
) : null}
</div>
{problem !== null ? (
<p class="ui-classifier-settings-note">{problem}</p>
) : null}
</form>
) : null}
</Panel>
);
}