feat(ui): confirm/keep auto-applied tags (milestone 139)
Auto-applied tags are provisional (they don't train the model + can be retracted until confirmed), so surface and confirm them: - Backend: list_for_image + get_image_with_tags now include `source` + a `confirmed` flag on each applied tag (via serialize_tag, image-scoped; defaulted for autocomplete/directory callers). - Frontend: TagChip badges an unconfirmed auto-tag with an "auto" pill + a one-click Keep/confirm (✓) → POST /images/<id>/tags/<id>/confirm, which promotes it to a training positive and shields it from the retraction sweep; TagPanel reloads so the badge + button drop once confirmed. Contract test for the source/confirmed payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -141,6 +141,38 @@ async def test_applied_tag_grounding_returns_winning_region(client, db):
|
||||
assert body["grounding"]["kind"] == "concept"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_applied_tags_expose_source_and_confirmed(client, db):
|
||||
# The chip UI needs each applied tag's source (auto vs manual) + confirmed
|
||||
# state to badge auto-tags and offer Keep/confirm (milestone 139).
|
||||
from backend.app.models import TagPositiveConfirmation
|
||||
from backend.app.models.tag import image_tag
|
||||
|
||||
img = ImageRecord(
|
||||
path="/images/srcflag.jpg", sha256="sf" * 32, size_bytes=1,
|
||||
mime="image/jpeg", width=1, height=1, origin="imported_filesystem",
|
||||
integrity_status="unknown",
|
||||
)
|
||||
db.add(img)
|
||||
await db.flush()
|
||||
auto = await TagService(db).find_or_create("autotag", TagKind.general)
|
||||
manual = await TagService(db).find_or_create("manualtag", TagKind.general)
|
||||
await db.execute(image_tag.insert().values(
|
||||
image_record_id=img.id, tag_id=auto.id, source="head_auto"))
|
||||
await db.execute(image_tag.insert().values(
|
||||
image_record_id=img.id, tag_id=manual.id, source="manual"))
|
||||
db.add(TagPositiveConfirmation(image_record_id=img.id, tag_id=manual.id))
|
||||
await db.commit()
|
||||
|
||||
resp = await client.get(f"/api/images/{img.id}/tags")
|
||||
assert resp.status_code == 200
|
||||
by_id = {t["id"]: t for t in await resp.get_json()}
|
||||
assert by_id[auto.id]["source"] == "head_auto"
|
||||
assert by_id[auto.id]["confirmed"] is False
|
||||
assert by_id[manual.id]["source"] == "manual"
|
||||
assert by_id[manual.id]["confirmed"] is True # has a confirmation row
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_applied_tag_grounding_no_head(client, db):
|
||||
# A tag with no head can't be localized → has_head False, grounding null; the
|
||||
|
||||
Reference in New Issue
Block a user