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:
1 parent
6a36f102c9
commit
8fb65d2de9
7 files changed
+110
-35
No files matched your search
+8
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,8 +54,42 @@ 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 (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<>
|
||||
<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>
|
||||
<th className="hint" style={{ fontWeight: 400, fontSize: '0.74rem', textAlign: 'left', padding: '2px 4px' }}>Class</th>
|
||||
@@ -110,6 +146,7 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user