feat: copy/paste per-class auto-annotate overrides as YAML (REQ-186, REQ-190)
Copy now emits YAML instead of the one-line "class <name> conf ..." text: one
block per selected class, fields named after the row labels, globals resolved
into the block, a name quoted only when YAML would misread it.
Paste reads the clipboard back and fills the overrides for the classes selected
in that modal, naming any it ignored — an override for a deselected class would
otherwise ride along in class_params unseen. container diffs go out as one
PATCH /projects/{id} and revert together if the save is rejected.
Parsing is parseClassYaml, a strict subset reader for exactly what Copy emits,
so no YAML dependency: any malformed line throws with its line number and
applies nothing. Values must look like numbers, not merely start like them —
parseFloat would have taken "0.5abc" as 0.5.
Both are real buttons with Lucide icons plus a status line reporting the
outcome, and both live in the shared table so the mass modal carries them too.
This commit is contained in:
1 parent
4f15335092
commit
4575c46f4c
8 files changed
+344
-49
No files matched your search
@@ -1405,6 +1405,58 @@ read it.
|
||||
(accent border + glow) — no CSS added; **browser eyeball still owed by the user**
|
||||
(375 px 2-col wrap, long class names).
|
||||
|
||||
## Task — Copy/Paste YAML for per-class overrides (REQ-186 amended, REQ-190) `[DONE]`
|
||||
|
||||
> Code, parser and build verified below; the **clipboard round trip in a real browser is still
|
||||
> owed** (the Clipboard API cannot be exercised from the shell).
|
||||
|
||||
1. **Icons** `frontend/src/components/Icons.jsx` — `CopyIcon` and `ClipboardPasteIcon` added
|
||||
in Lucide geometry, inheriting `currentColor`; the shared `Icon` wrapper already sets
|
||||
`aria-hidden` and `focusable="false"`.
|
||||
2. **Copy emits YAML** `frontend/src/components/ClassParamsTable.jsx` — `compose()` writes a
|
||||
`#` comment line plus one block per selected class, fields named from the row labels
|
||||
(`conf`/`iou`/`minbox`/`maxbox`/`container`) via `FIELD_BY_LABEL` so the wire format cannot
|
||||
drift from what is on screen. A name is quoted only when `PLAIN_NAME` fails, i.e. only when
|
||||
YAML would misread it. Effective-value resolution is unchanged (empty input still copies the
|
||||
global), so the block is a whole configuration rather than a diff.
|
||||
3. **Strict reader** `parseClassYaml()` in the same file, exported so it is testable without a
|
||||
DOM. Deliberately **not** `js-yaml`: a subset reader for exactly what Copy emits is ~35
|
||||
lines and needs no dependency. Rejects (with the line number) a list item, a nested map, a
|
||||
top-level scalar, an unknown key, a malformed number, a value outside 0–1 and an indented key
|
||||
before any class. Throwing happens before any state write, so a bad paste is atomic —
|
||||
nothing is half-applied.
|
||||
→ verify: **[DONE]** parsed the real parser through `esbuild --bundle --platform=node`: a
|
||||
two-class document with a quoted `"weird: name"` key round-trips to the internal key names
|
||||
(`threshold`, `iou_threshold`, `min_box_frac`, `max_box_frac`, `container`), and the malformed
|
||||
cases each throw the expected `line N: …` message (an empty document returns `{}`, which
|
||||
paste turns into "No classes in the pasted YAML"). A self-review pass caught a real hole the
|
||||
first harness run missed: `parseFloat('0.5abc')` is `0.5`, so a typo'd number was being
|
||||
accepted silently — exactly the failure the strict design exists to prevent. Fixed by
|
||||
matching the text against `NUMBER` before `parseFloat`. Same harness after the fix:
|
||||
`0.5abc`, `Infinity`, `NaN`, `0.5z`, `" 0.5 "` all throw; `1e-2`, tabs, CRLF, an inline
|
||||
`# comment` and a quoted name containing ` #` all parse. Repeated class blocks merge.
|
||||
4. **Paste** `frontend/src/components/ClassParamsTable.jsx` — reads the clipboard
|
||||
(`readClipboardText` in `frontend/src/clipboard.js`), applies only the classes selected in
|
||||
this modal, and names the ignored ones in the status line. `container` diffs are applied
|
||||
through `applyContainers()`, which folds the per-toggle logic into one `PATCH
|
||||
/projects/{id} {containers: {classId: bool}}` (`backend/projects.py:232` iterates the dict)
|
||||
and reverts all of them together on rejection, the same functional-revert trick the checkbox
|
||||
already used.
|
||||
→ verify: **[DONE]** `backend/projects.py` read: one PATCH covers N class ids; build green.
|
||||
**Owed, user browser**: copy → paste back → same numbers; paste a block for a deselected
|
||||
class → named as ignored and nothing written; `conf: 40` → `line 2: conf must be between 0
|
||||
and 1, got 40`, table untouched; container flag survives a modal reopen; serve over a LAN IP
|
||||
→ status line explains the blocked clipboard read.
|
||||
5. **Button design** `frontend/src/app.css` — `.class-params-tools` right-aligned wrapping row,
|
||||
`.btn` at `0.76rem` with `cursor: pointer` and a `progress` cursor while disabled (the old
|
||||
`btn-ghost` text button had neither), `.class-params-note` status line that turns `--danger`
|
||||
when the outcome failed. It does **not** use the app's `.hint` class: that is `--text-faint`,
|
||||
measured 2.68:1 on the modal panel, and the status line is small text that needs 4.5:1 —
|
||||
`--text-muted` measures 6.98:1 and `--danger` 4.71:1. Hover/focus-visible come from the
|
||||
shared `.btn` (`theme.css:114`, `:108`).
|
||||
→ verify: **[DONE]** `npm run build` green; contrast ratios computed from the tokens;
|
||||
`ClassParamsTable.jsx` 271 lines, inside the 400-line rule.
|
||||
|
||||
## Task — Configurable Track Forward + track-to-end (REQ-189) `[DONE]`
|
||||
|
||||
> Status caveat: code, build and docs are verified below; the **end-to-end GPU run in the
|
||||
|
||||
Reference in new issue
Block a user