feat(suggestions): user-managed blocklist for noisy auto-tags
Adds a tag_suggestion_blocklist table + service-layer filter so the
user can permanently suppress specific canonical tag names from the
suggestion stream (WD14 anatomy/composition tags like '1girl',
'breasts', 'nipples' that aren't useful for the discoverability use
case this app targets).
Data model
- migration k26042201: tag_suggestion_blocklist(name TEXT PK, created_at)
- model TagSuggestionBlocklistEntry
Service
- tag_suggestions._blocklisted_names() snapshots the current set
- get_suggestions filters merged results before grouping, so both
WD14- and embedding-sourced suggestions respect the blocklist
- get_bulk_suggestions inherits the filter via its per-image call
to get_suggestions
API
- GET /api/suggestions/blocklist -> {ok, names}
- POST /api/suggestions/blocklist -> add one
- POST /api/suggestions/blocklist/delete -> remove one
- POST /api/suggestions/blocklist/bulk -> replace the whole list
(backs the settings textarea save button)
UI
- modal suggestion chip gets a third action button (⊘) alongside
accept (✓) / reject (✕). Clicking it adds the name to the
blocklist, logs a rejection for ML feedback on this image, and
sweeps every chip on the page carrying that same name.
- Settings -> Maintenance -> Suggestion blocklist section with a
monospaced textarea (one name per line) + Save. Loads current
entries on mount.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -457,9 +457,71 @@
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h3>Suggestion blocklist</h3>
|
||||
<p class="settings-hint">
|
||||
One canonical tag name per line. Matches are exact against the
|
||||
post-canonicalization WD14 output (e.g. <code>1girl</code>,
|
||||
<code>breasts</code>). Blocklisted names never appear in the
|
||||
modal's auto-suggestions — either WD14 or embedding-similarity
|
||||
sourced. You can also add entries inline by clicking the ⊘ button
|
||||
on any suggestion chip.
|
||||
</p>
|
||||
<textarea id="suggestionBlocklistArea" class="settings-textarea" rows="10"
|
||||
placeholder="1girl 2girls breasts nipples penis"></textarea>
|
||||
<div class="mt-sm">
|
||||
<button type="button" id="suggestionBlocklistSaveBtn" class="btn secondary-btn">Save blocklist</button>
|
||||
<span id="suggestionBlocklistStatus" class="settings-hint" style="margin-left: 0.5rem;"></span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- end maintenance tab panel -->
|
||||
<script>
|
||||
(function(){
|
||||
const area = document.getElementById('suggestionBlocklistArea');
|
||||
const saveBtn = document.getElementById('suggestionBlocklistSaveBtn');
|
||||
const status = document.getElementById('suggestionBlocklistStatus');
|
||||
if (!area || !saveBtn) return;
|
||||
|
||||
async function load() {
|
||||
const r = await fetch('/api/suggestions/blocklist');
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
area.value = (j.names || []).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saveBtn.disabled = true;
|
||||
status.textContent = 'Saving…';
|
||||
const names = area.value.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
try {
|
||||
const r = await fetch('/api/suggestions/blocklist/bulk', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ names }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
area.value = (j.names || []).join('\n');
|
||||
status.textContent = `Saved ${j.count} entr${j.count === 1 ? 'y' : 'ies'}.`;
|
||||
} else {
|
||||
status.textContent = 'Save failed: ' + (j.error || 'unknown');
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = 'Save failed: ' + e;
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
setTimeout(() => { status.textContent = ''; }, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.addEventListener('click', save);
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user