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:
@@ -22,7 +22,15 @@ from sqlalchemy import Select, and_, distinct, exists, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from ..models import Artist, ImageProvenance, ImageRecord, Post, Source, Tag
|
||||
from ..models import (
|
||||
Artist,
|
||||
ImageProvenance,
|
||||
ImageRecord,
|
||||
Post,
|
||||
Source,
|
||||
Tag,
|
||||
TagPositiveConfirmation,
|
||||
)
|
||||
from ..models.tag import PRESENTATION_SYSTEM_TAGS, image_tag
|
||||
from .pagination import decode_cursor, encode_cursor
|
||||
from .tag_query import (
|
||||
@@ -731,8 +739,14 @@ class GalleryService:
|
||||
# Self-join Tag to resolve a character's fandom NAME (not just id) so the
|
||||
# modal chip can label it without an N+1 (shared tag_query helpers).
|
||||
fandom_alias = fandom_join_alias()
|
||||
# source drives the auto-applied badge; confirmed = operator affirmed the
|
||||
# tag (positive + retraction-shielded, milestone 139).
|
||||
confirmed = exists().where(
|
||||
TagPositiveConfirmation.image_record_id == image_id,
|
||||
TagPositiveConfirmation.tag_id == Tag.id,
|
||||
).label("confirmed")
|
||||
tag_stmt = (
|
||||
select(*tag_columns(fandom_alias))
|
||||
select(*tag_columns(fandom_alias), image_tag.c.source, confirmed)
|
||||
.select_from(
|
||||
Tag.__table__
|
||||
.join(image_tag, image_tag.c.tag_id == Tag.id)
|
||||
|
||||
@@ -91,4 +91,11 @@ def serialize_tag(row) -> dict:
|
||||
"fandom_id": row.fandom_id,
|
||||
"fandom_name": row.fandom_name,
|
||||
"is_system": bool(getattr(row, "is_system", False)),
|
||||
# Applied-tag context: only the image-scoped selects (list_for_image /
|
||||
# get_image_with_tags) provide these; autocomplete / directory don't →
|
||||
# default. `source` drives the auto-applied badge; `confirmed` = the
|
||||
# operator affirmed the tag (a training positive, shielded from the
|
||||
# retraction sweep — milestone 139).
|
||||
"source": getattr(row, "source", None),
|
||||
"confirmed": bool(getattr(row, "confirmed", False)),
|
||||
}
|
||||
|
||||
@@ -9,7 +9,14 @@ from sqlalchemy import and_, case, exists, func, select, text, update
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..models import HeadMetric, Tag, TagHead, TagKind, image_tag
|
||||
from ..models import (
|
||||
HeadMetric,
|
||||
Tag,
|
||||
TagHead,
|
||||
TagKind,
|
||||
TagPositiveConfirmation,
|
||||
image_tag,
|
||||
)
|
||||
from .db_helpers import get_or_create
|
||||
from .tag_query import fandom_join_alias, tag_columns
|
||||
|
||||
@@ -288,8 +295,14 @@ class TagService:
|
||||
character chip with its fandom without an N+1 (mirrors the
|
||||
autocomplete/directory resolution)."""
|
||||
fandom_alias = fandom_join_alias()
|
||||
# source drives the auto-applied badge; confirmed = operator affirmed the
|
||||
# tag (positive + retraction-shielded, milestone 139).
|
||||
confirmed = exists().where(
|
||||
TagPositiveConfirmation.image_record_id == image_id,
|
||||
TagPositiveConfirmation.tag_id == Tag.id,
|
||||
).label("confirmed")
|
||||
stmt = (
|
||||
select(*tag_columns(fandom_alias))
|
||||
select(*tag_columns(fandom_alias), image_tag.c.source, confirmed)
|
||||
.select_from(
|
||||
Tag.__table__
|
||||
.join(image_tag, image_tag.c.tag_id == Tag.id)
|
||||
|
||||
Reference in New Issue
Block a user