Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/pages/review-settings/ClassifierKey.tsx
iris 6875a4f0f8 swarm-ui: set the review classifier's keys from the classifier settings
The classifier's Anthropic and ACP provider keys were written by hand in the
secret store's UI. The controller now stores them: GET
/api/review-classifier/keys says whether each is set and when it changed, and
PUT /api/review-classifier/keys/{runtime} writes one. No route answers a key.

The controller's store policy gains create and update on the two leaves with
no read of their values, and a metadata read for the set state. The swarm UI's
classifier settings card gets a key form beside the settings, saved apart
because it goes to the secret store.
2026-11-11 19:01:01 +01:00

127 lines
4 KiB
TypeScript

// <ClassifierKey> — the key the chosen runtime runs on, set from here instead
// of in the bao UI (`GET /api/review-classifier/keys`, `PUT .../keys/{runtime}`).
// The page can say whether a key is stored and when it changed, never what it
// is: the field takes a new key and is cleared once it's saved. A key is
// saved apart from the settings, because it goes to the secret store and not
// into the settings document.
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 { ActionRow } from "../../ui/action-row/ActionRow.js";
import { Button } from "../../ui/button/Button.js";
import { Hint } from "../../ui/hint/Hint.js";
import { TextField } from "../../ui/text-field/TextField.js";
import { keyOf, keyProblem, keyStatus, type KeysBody } from "./KeyTypes.js";
import type { Runtime } from "./SettingsTypes.js";
const KEYS = "/api/review-classifier/keys";
export function ClassifierKey({ runtime }: { runtime: Runtime }) {
const [keys, setKeys] = useState<KeysBody | null>(null);
const [loadError, setLoadError] = useState<ProblemDetails | null>(null);
const [saveError, setSaveError] = useState<ProblemDetails | null>(null);
// The key as typed. Never kept after a save.
const [text, setText] = useState("");
const [saving, setSaving] = useState(false);
const [justSaved, setJustSaved] = useState(false);
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const res = await fetch(KEYS);
if (cancelled) return;
if (!res.ok) {
setLoadError(await readApiError(res));
return;
}
const body = (await res.json()) as KeysBody;
if (!cancelled) setKeys(body);
} catch (e: unknown) {
if (!cancelled) setLoadError({ detail: String(e) });
}
})();
return () => {
cancelled = true;
};
}, []);
// A key typed for one runtime must not be saved to the other.
useEffect(() => {
setText("");
setSaveError(null);
setJustSaved(false);
}, [runtime]);
const problem = keyProblem(text);
async function save(e: Event) {
e.preventDefault();
if (problem !== null) return;
setSaving(true);
setSaveError(null);
setJustSaved(false);
try {
const res = await fetch(`${KEYS}/${runtime}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ api_key: text.trim() }),
});
if (!res.ok) {
setSaveError(await readApiError(res));
return;
}
setKeys((await res.json()) as KeysBody);
setText("");
setJustSaved(true);
} catch (err: unknown) {
setSaveError({ detail: String(err) });
} finally {
setSaving(false);
}
}
const label = runtime === "acp" ? "provider key" : "Anthropic API key";
return (
<form class="ui-classifier-settings" onSubmit={(e) => void save(e)}>
{loadError ? (
<ApiErrorPanel
context="the key could not be read"
problem={loadError}
/>
) : null}
<TextField
id="classifier-key"
label={`${label}${keys ? ` (${keyStatus(keyOf(keys, runtime))})` : ""}`}
type="password"
value={text}
placeholder={
keys && keyOf(keys, runtime).set
? "paste a new key to replace it"
: "paste the key"
}
onInput={(t) => {
setText(t);
setJustSaved(false);
}}
/>
{saveError ? (
<ApiErrorPanel context="the key was not saved" problem={saveError} />
) : null}
<ActionRow>
<Button
variant="primary"
type="submit"
disabled={saving || problem !== null}
>
{saving ? "saving…" : "save key"}
</Button>
{justSaved ? (
<Hint as="span" role="status">
Saved. The next run uses it.
</Hint>
) : null}
</ActionRow>
</form>
);
}