feat: one-click copy of effective auto-annotate params (REQ-186)

- ClassParamsTable: Copy button above per-class overrides table —
  clipboard text, one line per class (class/conf/iou/minbox/container),
  effective values (override else global), present in both the
  per-batch and mass auto-annotate modals via the shared component
- clipboard.js: extract legacyCopy + copyText from ArchiveControls
  (insecure-context execCommand fallback), ArchiveControls now imports it
- docs: REQ-186 added; design/ui-spec/tasks synced
This commit is contained in:
asus committed 2026-10-02 14:28:46 +07:00
1 parent 6a36f102c9
commit 8fb65d2de9
7 files changed
+108 -33

No files matched your search

+8 -2
View File
@@ -331,7 +331,7 @@ Pages:
The canvas editor is hand-written; the normalized-coordinate conventions already exist in
`sessions.py` (`annotations_payload`, `detections_payload`) as a reference.
**Auto-annotate modal (REQ-171, REQ-172, REQ-182, REQ-184).** `AutoAnnotateModal.jsx` splits into
**Auto-annotate modal (REQ-171, REQ-172, REQ-182, REQ-184, REQ-186).** `AutoAnnotateModal.jsx` splits into
`PreviewShapes.jsx` (the result overlay, shared with the mass modal),
`ClassPromptPanel.jsx` (class chips + the editable SAM3 prompt) and `ExemplarCanvas.jsx`
(the drag-to-draw layer). All three overlays and the `<img>` share one shrink-wrapped
@@ -353,7 +353,13 @@ plain full-set preview (the session's last full-set run, blank if none ran yet);
Run Preview with examples runs the exemplared classes
sequentially and merges, without examples it is the unchanged full-set request. The
per-class overrides table also carries a **Container** checkbox (REQ-184) that toggles
`project_classes.container` through `PATCH /api/projects/{id} { containers: … }`.
`project_classes.container` through `PATCH /api/projects/{id} { containers: … }`. The same
table carries a **Copy** button (REQ-186): one click puts every selected class's effective
settings (per-class override where set, else the global slider; container = the checkbox
state) on the clipboard as short lines `class <name> conf … iou … minbox … container
true|false`, through `clipboard.js`'s `copyText` (Clipboard API with an `execCommand`
fallback for insecure contexts) — read-only, no network, no setting changed. Because
`ClassParamsTable` is shared, the mass modal carries the button too.
**Review sidebar class rows (REQ-180, REQ-183, REQ-185).** Each class row's frame-clear `×`
(REQ-180) gains an eye toggle in front of it: session-only `Set` state in `ReviewPage`, so it
+8
View File
@@ -135,6 +135,14 @@ changes.
without examples contribute nothing while any example exists, and clearing the last example
restores the plain full-set preview. The preview and the batch job still use the same
thresholds and per-class overrides.
- **REQ-186** — The auto-annotate parameters table has a **copy button** that puts every
selected class's effective settings on the clipboard as short text, one class per line:
`class <name> conf <v> iou <v> minbox <v> container <true|false>`. Effective means the
per-class override where one is set, otherwise the global slider value; `container` is
the class's current checkbox state. The button sits in the shared per-class table, so it
exists in both the per-batch auto-annotate modal and the mass (batches tab) modal, and
the copied text is a plain baseline for later tuning — no structure beyond the line
format. Copying needs no network round-trip and never changes any setting.
## E. Review & correction
+11
View File
@@ -1337,6 +1337,17 @@ read it.
that the removed purge effect (`while a class is hidden`) has zero hits left in
`ReviewPage.jsx`; browser `H`/eye click-test is NOT automated — manual click-test pending.
## Task — Copy effective auto-annotate params (REQ-186) `[DONE]`
1. Copy button in the shared per-class table → clipboard text, one line per class,
effective values, container true/false, both modals → verify: **[DONE]** `cd frontend
&& npm run build` passes (✓ 528 ms); `wc -l` — `frontend/src/components/ClassParamsTable.jsx`
≤ 400 (152), `frontend/src/clipboard.js` (36); `git diff --stat` = exactly 3 code
files (clipboard.js, ArchiveControls.jsx, ClassParamsTable.jsx) with ArchiveControls
extraction-only; format walkthrough byte-exact vs REQ-186 sample lines (empty→global,
invalid→global, float-noise case 0.30000000000000004 → `0.3`); browser clipboard
click-test NOT automated — manual click-test pending.
## Known open points
- *Not closed by any task, by choice:* **any rebuild kills the running job.** Task 14's resume
+6
View File
@@ -462,6 +462,12 @@ 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 table; 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 container false` — an empty input copies the
global value. Present in the engine-chooser modal **and** the mass modal (shared
component).
- `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.
+36
View File
@@ -0,0 +1,36 @@
// Clipboard API needs a secure context — on http://<ip>:9000 it is undefined.
// execCommand('copy') still works there inside a click gesture; if both fail,
// the path is revealed in a pre-selected field so one Ctrl+C always works.
function legacyCopy(text) {
const area = document.createElement('textarea')
area.value = text
area.setAttribute('readonly', '')
area.style.position = 'fixed'
area.style.opacity = '0'
document.body.appendChild(area)
area.select()
let ok = false
try {
ok = document.execCommand('copy')
} catch {
ok = false
}
document.body.removeChild(area)
return ok
}
export async function copyText(text) {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
return true
}
} catch {
// fall through to legacyCopy below
}
try {
return legacyCopy(text)
} catch {
return false
}
}
+2 -31
View File
@@ -1,29 +1,9 @@
import { useRef, useState } from 'react'
import { api } from '../api'
import { copyText } from '../clipboard'
const ACCEPT = '.mp4,.mkv,.mov,.avi,.webm,.m4v'
// Clipboard API needs a secure context — on http://<ip>:9000 it is undefined.
// execCommand('copy') still works there inside a click gesture; if both fail,
// the path is revealed in a pre-selected field so one Ctrl+C always works.
function legacyCopy(text) {
const area = document.createElement('textarea')
area.value = text
area.setAttribute('readonly', '')
area.style.position = 'fixed'
area.style.opacity = '0'
document.body.appendChild(area)
area.select()
let ok = false
try {
ok = document.execCommand('copy')
} catch {
ok = false
}
document.body.removeChild(area)
return ok
}
function CopyPath({ label, value }) {
const [copied, setCopied] = useState(false)
const [revealed, setRevealed] = useState(false)
@@ -35,16 +15,7 @@ function CopyPath({ label, value }) {
}
const copy = async () => {
let ok = false
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value)
ok = true
}
} catch {
ok = false
}
if (!ok) ok = legacyCopy(value)
const ok = await copyText(value)
if (ok) finish()
else setRevealed(true)
}
@@ -1,5 +1,6 @@
import React, { useState } from 'react'
import { api } from '../api'
import { copyText } from '../clipboard'
// Per-class overrides of the job's globals (REQ-181). Empty input = inherit.
const KEYS = [
@@ -30,6 +31,7 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
const [containers, setContainers] = useState(() =>
new Set((project?.classes || []).filter(c => c.container).map(c => c.name))
)
const [copied, setCopied] = useState(false)
const set = (name, key, raw) =>
onChange({ ...value, [name]: { ...(value[name] || {}), [key]: raw } })
const toggleContainer = async (cls) => {
@@ -52,7 +54,41 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
}
}
if (!classNames.length) return null
const fmt = (v) => String(Math.round(v * 1000) / 1000)
const effective = (name, key) => {
const raw = value[name]?.[key]
const num = raw === '' || raw == null ? NaN : parseFloat(raw)
return Number.isFinite(num) ? num : globals[key]
}
const compose = () =>
classNames
.map(
(name) =>
`class ${name} conf ${fmt(effective(name, 'threshold'))} iou ${fmt(
effective(name, 'iou_threshold')
)} minbox ${fmt(effective(name, 'min_box_frac'))} container ${containers.has(name)}`
)
.join('\n')
const copy = async () => {
const ok = await copyText(compose())
if (ok) {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} else {
console.error('Could not copy the effective auto-annotate parameters')
}
}
return (
<>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
className="btn btn-ghost"
onClick={copy}
style={{ fontSize: '0.76rem', padding: '4px 8px', cursor: 'pointer' }}
>
<span aria-live="polite">{copied ? 'Copied' : 'Copy'}</span>
</button>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
@@ -111,5 +147,6 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
})}
</tbody>
</table>
</>
)
}