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 (
-