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
+108
-33
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
|
The canvas editor is hand-written; the normalized-coordinate conventions already exist in
|
||||||
`sessions.py` (`annotations_payload`, `detections_payload`) as a reference.
|
`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),
|
`PreviewShapes.jsx` (the result overlay, shared with the mass modal),
|
||||||
`ClassPromptPanel.jsx` (class chips + the editable SAM3 prompt) and `ExemplarCanvas.jsx`
|
`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
|
(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
|
Run Preview with examples runs the exemplared classes
|
||||||
sequentially and merges, without examples it is the unchanged full-set request. The
|
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
|
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 `×`
|
**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
|
(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
|
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
|
restores the plain full-set preview. The preview and the batch job still use the same
|
||||||
thresholds and per-class overrides.
|
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
|
## E. Review & correction
|
||||||
|
|
||||||
|
|||||||
@@ -1337,6 +1337,17 @@ read it.
|
|||||||
that the removed purge effect (`while a class is hidden`) has zero hits left in
|
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.
|
`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
|
## Known open points
|
||||||
|
|
||||||
- *Not closed by any task, by choice:* **any rebuild kills the running job.** Task 14's resume
|
- *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
|
unmarked classes are governed by IoU alone. Toggling is optimistic
|
||||||
`PATCH /projects/{id} { containers: { [classId]: bool } }` into `project_classes.container`
|
`PATCH /projects/{id} { containers: { [classId]: bool } }` into `project_classes.container`
|
||||||
with revert on rejection — preview and batch job read the same stored flag.
|
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`:
|
- `ClassPromptPanel`:
|
||||||
- Target-class chips. Without SAM3 a chip is a simple toggle.
|
- 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.
|
- **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 { useRef, useState } from 'react'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
import { copyText } from '../clipboard'
|
||||||
|
|
||||||
const ACCEPT = '.mp4,.mkv,.mov,.avi,.webm,.m4v'
|
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 }) {
|
function CopyPath({ label, value }) {
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [revealed, setRevealed] = useState(false)
|
const [revealed, setRevealed] = useState(false)
|
||||||
@@ -35,16 +15,7 @@ function CopyPath({ label, value }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
let ok = false
|
const ok = await copyText(value)
|
||||||
try {
|
|
||||||
if (navigator.clipboard?.writeText) {
|
|
||||||
await navigator.clipboard.writeText(value)
|
|
||||||
ok = true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
if (!ok) ok = legacyCopy(value)
|
|
||||||
if (ok) finish()
|
if (ok) finish()
|
||||||
else setRevealed(true)
|
else setRevealed(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
import { copyText } from '../clipboard'
|
||||||
|
|
||||||
// Per-class overrides of the job's globals (REQ-181). Empty input = inherit.
|
// Per-class overrides of the job's globals (REQ-181). Empty input = inherit.
|
||||||
const KEYS = [
|
const KEYS = [
|
||||||
@@ -30,6 +31,7 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
|
|||||||
const [containers, setContainers] = useState(() =>
|
const [containers, setContainers] = useState(() =>
|
||||||
new Set((project?.classes || []).filter(c => c.container).map(c => c.name))
|
new Set((project?.classes || []).filter(c => c.container).map(c => c.name))
|
||||||
)
|
)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
const set = (name, key, raw) =>
|
const set = (name, key, raw) =>
|
||||||
onChange({ ...value, [name]: { ...(value[name] || {}), [key]: raw } })
|
onChange({ ...value, [name]: { ...(value[name] || {}), [key]: raw } })
|
||||||
const toggleContainer = async (cls) => {
|
const toggleContainer = async (cls) => {
|
||||||
@@ -52,7 +54,41 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!classNames.length) return null
|
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 (
|
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' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -111,5 +147,6 @@ export default function ClassParamsTable({ classNames, globals, value, onChange,
|
|||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user