From 0e3437dcafa671781b1a5ae7fd83ffc172b91bb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 18:52:56 +0000 Subject: [PATCH] feat: remove selected items from a training set Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MpnF7GwNGMbngU11wjym7P --- app/ui/features/training/tags.js | 10 +++++++++- app/ui/features/training/training.test.js | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/ui/features/training/tags.js b/app/ui/features/training/tags.js index f3f26a7f..71a36403 100644 --- a/app/ui/features/training/tags.js +++ b/app/ui/features/training/tags.js @@ -41,6 +41,14 @@ export function tagsStep({ app, session, go, ui, peek }) { const filter = c.segmented.sm({ label: "Show", options: FILTERS, value: ui.filter, onChange: (v) => { ui.filter = v; ui.limit = PAGE; ui.redraw(); } }); const everyone = c.button.sm({ label: "Select all shown", tone: "ghost", onClick: () => { list.forEach((i) => sel.add(i.id)); ui.redraw(); } }); const none = c.button.sm({ label: "Clear", tone: "ghost", onClick: () => { sel.clear(); ui.redraw(); } }); + const drop = c.button.sm({ label: "Remove selected", tone: "danger", title: "Take the selected items out of this set. The files themselves stay where they are.", onClick: async () => { + if (!sel.size) { c.toast.info({ text: "Tick the items to remove first." }); return; } + const n = sel.size; + if (!(await c.modal.dialogue({ title: "Remove from set", message: `Remove ${n} item${n > 1 ? "s" : ""} from "${session.name}"? The files themselves are not deleted.`, tone: "danger", confirmLabel: "Remove" }).result)) return; + session.edit((d) => { d.items = d.items.filter((i) => !sel.has(i.id)); }); + sel.clear(); + ui.redraw(); + } }); // --- bulk edits on the selected (or all, when nothing is selected) const tagBox = c.input.md({ label: "Tag", placeholder: "tag" }); @@ -78,7 +86,7 @@ export function tagsStep({ app, session, go, ui, peek }) { ] }), c.group.default({ label: "Automatic tagging", rows: [autoTag({ app, session, ui })] }), c.group.default({ label: "Tags in this set", hint: "Click one to show only the items that have it. The x removes it from all.", rows: [cloudEl] }), - c.toolbar.default({ items: [filter, count], trailing: [everyone, none] }), + c.toolbar.default({ items: [filter, count], trailing: [everyone, none, drop] }), bar, grid, more, c.toolbar.default({ items: [], trailing: [c.button.md({ label: "Back", tone: "ghost", onClick: () => go("data") }), c.button.md({ label: "Next: train", tone: "primary", onClick: () => go("train") })] }), diff --git a/app/ui/features/training/training.test.js b/app/ui/features/training/training.test.js index 6bd5e04f..5e73aca0 100644 --- a/app/ui/features/training/training.test.js +++ b/app/ui/features/training/training.test.js @@ -231,3 +231,22 @@ test("rated results are added with the captions the server built, and changing w assert.equal(added[1][0].caption, "a red fox 35mm"); off(); }); + +test("Tags step: Remove selected takes the ticked items out of the set and saves", async () => { + const f = fake(); + const { button, off } = await open(f); + button.click(); + await wait(60); + click("Next: tags").click(); + await wait(20); + document.querySelector(".cx-captioned-pick").click(); + click("Remove selected").click(); + await wait(40); + const yes = [...document.querySelectorAll("button")].filter((b) => b.textContent.trim() === "Remove").pop(); + yes.click(); + await wait(700); + const saved = f.calls.filter((c) => c[0] === "save").pop(); + assert.equal(saved[2].items.length, 1); + assert.equal(saved[2].items[0].id, "cccccccccccc"); + off(); +});