feat(suggestions): group wip/banner/editor under a separate 'system' category
System tags are kind=general, so their suggestions previously landed in the General group. Give them their own 'system' suggestion category so the operator reviews them apart from content tags: _current_heads maps is_system heads to category 'system' (still trained as general heads, still gated by the 0.65 floor). Frontend: CATEGORY_ORDER/LABELS gain 'system'; SuggestionsPanel renders a 'System' group first (small, collapsible, open — false positives easy to spot and reject); the typed-dropdown shows the shield icon for system entries. Safe: system-tag suggestions always carry a canonical_tag_id, so the create-by-kind path (which would send 'system' as a TagKind) is never hit. 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:
@@ -326,7 +326,10 @@ async def _current_heads(session: AsyncSession, embedding_version: str):
|
|||||||
{
|
{
|
||||||
"tag_id": r.tag_id,
|
"tag_id": r.tag_id,
|
||||||
"name": r.name,
|
"name": r.name,
|
||||||
"category": _CATEGORY.get(r.kind, "general"),
|
# System tags (wip/banner/editor) are kind=general but group under
|
||||||
|
# their OWN "system" suggestion category so the operator reviews
|
||||||
|
# them apart from content tags (they still train as general heads).
|
||||||
|
"category": "system" if r.is_system else _CATEGORY.get(r.kind, "general"),
|
||||||
"auto_apply_threshold": r.auto_apply_threshold,
|
"auto_apply_threshold": r.auto_apply_threshold,
|
||||||
"is_system": bool(r.is_system),
|
"is_system": bool(r.is_system),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,16 @@
|
|||||||
of the rail (ImageViewer side layout), so a long suggestion set no
|
of the rail (ImageViewer side layout), so a long suggestion set no
|
||||||
longer needs an internal scroll cap to keep Related reachable. -->
|
longer needs an internal scroll cap to keep Related reachable. -->
|
||||||
<div v-else class="fc-suggestions__list">
|
<div v-else class="fc-suggestions__list">
|
||||||
|
<!-- System hygiene tags (wip/banner/editor) surface in their own group,
|
||||||
|
first, so false positives are easy to spot and reject (reject-to-train
|
||||||
|
for these heads). Small set — collapsible, open by default. -->
|
||||||
|
<SuggestionsCategoryGroup
|
||||||
|
v-if="store.byCategory.system && store.byCategory.system.length"
|
||||||
|
label="System" :items="store.byCategory.system"
|
||||||
|
collapsible :default-open="true"
|
||||||
|
@accept="onAccept" @alias="onAlias" @remove-alias="onRemoveAlias"
|
||||||
|
@dismiss="onDismiss" @undismiss="onUndismiss"
|
||||||
|
/>
|
||||||
<SuggestionsCategoryGroup
|
<SuggestionsCategoryGroup
|
||||||
v-for="cat in peopleCats" :key="cat"
|
v-for="cat in peopleCats" :key="cat"
|
||||||
v-show="store.byCategory[cat] && store.byCategory[cat].length"
|
v-show="store.byCategory[cat] && store.byCategory[cat].length"
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ const KNOWN_KINDS = new Set([
|
|||||||
const KIND_ICONS = {
|
const KIND_ICONS = {
|
||||||
general: 'mdi-tag', character: 'mdi-account-circle',
|
general: 'mdi-tag', character: 'mdi-account-circle',
|
||||||
fandom: 'mdi-book-open-page-variant', series: 'mdi-bookshelf',
|
fandom: 'mdi-book-open-page-variant', series: 'mdi-bookshelf',
|
||||||
|
// 'system' is a suggestion category (wip/banner/editor), not a tag kind —
|
||||||
|
// matches the shield marker on system-tag chips.
|
||||||
|
system: 'mdi-shield-outline',
|
||||||
}
|
}
|
||||||
function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { useApi } from '../composables/useApi.js'
|
|||||||
import { useAsyncAction } from '../composables/useAsyncAction.js'
|
import { useAsyncAction } from '../composables/useAsyncAction.js'
|
||||||
import { useInflightToken } from '../composables/useInflightToken.js'
|
import { useInflightToken } from '../composables/useInflightToken.js'
|
||||||
|
|
||||||
// Category display order: people first, general last.
|
// Category display order: system (hygiene flags) first for quick review, then
|
||||||
// 'artist' (FC-2d-vii-c) and 'copyright' (2026-06-01) retired — only
|
// people, general last. 'artist' (FC-2d-vii-c) and 'copyright' (2026-06-01)
|
||||||
// character and general surface as suggestion categories now.
|
// retired. 'system' groups the wip/banner/editor hygiene tags apart from content
|
||||||
export const CATEGORY_ORDER = ['character', 'general']
|
// tags (they're kind=general on the backend but surface under their own category).
|
||||||
|
export const CATEGORY_ORDER = ['system', 'character', 'general']
|
||||||
export const CATEGORY_LABELS = {
|
export const CATEGORY_LABELS = {
|
||||||
|
system: 'System',
|
||||||
character: 'Character',
|
character: 'Character',
|
||||||
general: 'General'
|
general: 'General'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,9 +124,11 @@ async def test_system_tag_surfaces_at_flat_floor_despite_high_threshold(db):
|
|||||||
await _head(db, tag.id, slot=0, suggest_threshold=0.9)
|
await _head(db, tag.id, slot=0, suggest_threshold=0.9)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
sl = await SuggestionService(db).for_image(img.id)
|
sl = await SuggestionService(db).for_image(img.id)
|
||||||
|
# System tags surface under their OWN "system" category, not "general".
|
||||||
assert any(
|
assert any(
|
||||||
s.canonical_tag_id == tag.id for s in sl.by_category.get("general", [])
|
s.canonical_tag_id == tag.id for s in sl.by_category.get("system", [])
|
||||||
)
|
)
|
||||||
|
assert not sl.by_category.get("general")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -139,7 +141,7 @@ async def test_system_tag_below_floor_stays_hidden(db):
|
|||||||
await _head(db, tag.id, slot=0, suggest_threshold=0.9)
|
await _head(db, tag.id, slot=0, suggest_threshold=0.9)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
sl = await SuggestionService(db).for_image(img.id)
|
sl = await SuggestionService(db).for_image(img.id)
|
||||||
assert not sl.by_category.get("general")
|
assert not sl.by_category.get("system")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user