diff --git a/docs/design.md b/docs/design.md index 9d3d86c..ff2b038 100644 --- a/docs/design.md +++ b/docs/design.md @@ -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 `` 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 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 diff --git a/docs/requirements.md b/docs/requirements.md index e77a0df..d318365 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -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 conf iou minbox container `. 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 diff --git a/docs/tasks.md b/docs/tasks.md index 936f671..d8cf2e7 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -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 diff --git a/docs/ui-spec.md b/docs/ui-spec.md index 66b8d34..4b003a5 100644 --- a/docs/ui-spec.md +++ b/docs/ui-spec.md @@ -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. diff --git a/frontend/src/clipboard.js b/frontend/src/clipboard.js new file mode 100644 index 0000000..72e8c12 --- /dev/null +++ b/frontend/src/clipboard.js @@ -0,0 +1,36 @@ +// Clipboard API needs a secure context — on http://: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 + } +} diff --git a/frontend/src/components/ArchiveControls.jsx b/frontend/src/components/ArchiveControls.jsx index 9512a0d..8a92cc1 100644 --- a/frontend/src/components/ArchiveControls.jsx +++ b/frontend/src/components/ArchiveControls.jsx @@ -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://: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) } diff --git a/frontend/src/components/ClassParamsTable.jsx b/frontend/src/components/ClassParamsTable.jsx index 3fc9386..1fae356 100644 --- a/frontend/src/components/ClassParamsTable.jsx +++ b/frontend/src/components/ClassParamsTable.jsx @@ -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 ( - + <> +
+ +
+
@@ -110,6 +146,7 @@ export default function ClassParamsTable({ classNames, globals, value, onChange, ) })} -
Class
+ + ) }