diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 2e60a130..5b7f14ca 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -64,8 +64,9 @@ and the head commit the run read. A failed run shows its error, for example when 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 nothing and posts nothing to the forge. It shows no token -counts, cache counts or cost. +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 diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx index fa52c568..4b353b07 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/ReviewTryOutPage.tsx @@ -7,8 +7,9 @@ // `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 stores nothing (not even in the browser) and posts nothing -// to the forge. +// 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"; @@ -19,6 +20,7 @@ import { ClassificationEditor } from "./ClassificationEditor.js"; import { TryOutResult } from "./TryOutResult.js"; import { draftProblem, + duplicateName, prNumber, repoProblem, toBody, @@ -29,8 +31,9 @@ import "./ReviewTryOutPage.css"; const TRY_OUT = "/api/review-classifier/try-out"; const POLL_MS = 2000; -// More classifications than this are not offered a row; the backend has its -// own cap and says so with a 400, which the page shows as it is. +// More classifications than this are not offered a row. Keep it in step with +// the try-out endpoint's own cap, which answers a longer list with a 400 that +// the page shows as it is; the backend has no way to report it yet. const MAX_CLASSIFICATIONS = 10; let nextKey = 0; @@ -115,6 +118,7 @@ export function ReviewTryOutPage() { 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) { diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx index 668e1f30..771f52ec 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutResult.tsx @@ -51,7 +51,9 @@ export function TryOutResult({ result }: { result: Result }) { r.name} + // 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" /> diff --git a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts index 94d0de63..3c3a313b 100644 --- a/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts +++ b/frontend/packages/swarm-ui/src/pages/review-tryout/TryOutTypes.ts @@ -81,6 +81,18 @@ export function draftProblem(d: ClassificationDraft): string | null { 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(); + 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(),