From a4a4381cf652b40c054f54fc07b6d8ecb2abcaf8 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 12 Oct 2026 01:08:58 +0200 Subject: [PATCH] swarm-ui: try-out rows keyed by position, distinct names, and accurate storage wording Two classifications sharing a name made the result table's row key collide, so the form now refuses duplicate names and the table keys rows by position anyway. The page said it stores nothing in the browser, but the result table remembers its column filters like every table does; the header comment and the docs now say that. The row cap's comment says to keep it in step with the endpoint's own cap. --- docs/swarm/ui.md | 5 +++-- .../src/pages/review-tryout/ReviewTryOutPage.tsx | 12 ++++++++---- .../src/pages/review-tryout/TryOutResult.tsx | 4 +++- .../swarm-ui/src/pages/review-tryout/TryOutTypes.ts | 12 ++++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) 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(),