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:
parent
37c58e79dc
commit
baf282131f
1 changed files with 293 additions and 3 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue