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:
asus committed 2026-10-05 14:17:17 +07:00
1 parent 4f15335092
commit 4575c46f4c
8 files changed
+344 -49

No files matched your search

+35 -6
View File
@@ -466,12 +466,41 @@ Two columns inside one dialog (920 px wide, max 96 vw / 90 vh, scrollable).
unmarked classes are governed by IoU alone. Toggling is optimistic
`PATCH /projects/{id} { containers: { [classId]: bool } }` into `project_classes.container`
with revert on rejection — preview and batch job read the same stored flag.
- **Copy (REQ-186)** — small right-aligned ghost button above the overrides block; label
`Copy`, flips to `Copied` for 1.5 s inside an `aria-live` span, and on failure silently
stays `Copy`. One line per selected class, e.g.
`class sack conf 0.4 iou 0.6 minbox 0.005 maxbox 1 container false` — an empty input copies the
global value. Present in the engine-chooser modal **and** the mass modal (shared
component).
- **Copy / Paste YAML (REQ-186, REQ-190)** — a small right-aligned toolbar above the overrides
block: two real buttons, `Copy YAML` and `Paste YAML`, each an inline SVG icon (Lucide copy /
clipboard-paste, no emoji) plus its label, `btn` styling at `0.76rem`, both disabled while a
paste is in flight. A `role="status"` line beside them carries the outcome — `Copied`,
`Pasted 3 classes`, `Pasted 2 classes — ignored crane`, or the failure in `--danger` — success
text clears itself after 2 s, a failure stays until the next action.
Copy puts one YAML block per selected class on the clipboard:
```yaml
# auto-annotate per-class overrides
sack:
conf: 0.4
iou: 0.6
minbox: 0.005
maxbox: 1
container: false
```
Field names are the row labels as written on screen; a name is quoted only when YAML would
misread it (`"weird: name":`). An empty input copies the global value, so the block is a
complete configuration, not a diff.
Paste reads the clipboard and fills the overrides for the classes **selected in this modal**;
an unselected class is named in the status line and never written. Numbers merge per field.
`container` is saved to the server like ticking the box by hand (one PATCH for all changed
classes, reverted together on rejection, and the status line says the flags were not saved).
Parsing is strict and **atomic**: `#` comments (whole lines, or trailing on a value), `class:`
at the left margin, `key: value` indented under it, numbers in 0–1 written as numbers
(`0.5abc` is a typo, not `0.5`), `container` `true|false|yes|no|1|0`; a list item, a nested
map, a top-level scalar, an unknown key, a malformed number, an out-of-range value or a
key before any class throws `line N: …` and applies **nothing**. A class repeated in the same
document merges rather than replacing. Where the browser blocks the clipboard read — the
app on plain http over a LAN address, or the permission prompt refused — the status line says
so rather than doing nothing.
Both buttons live in the shared per-class table, so they exist in the engine-chooser modal
**and** the mass modal.
- `ClassPromptPanel`:
- Target-class chips. Without SAM3 a chip is a simple toggle.
- **With SAM3 a chip is two buttons**: the *name* selects-and-activates, the `×` deselects.