feat(bulk): consensus ML suggestions + frontend polish
Bulk editor now loads consensus ML suggestions across the current selection via a new POST /api/suggestions/bulk endpoint, powered by get_bulk_suggestions() in tag_suggestions.py. A tag is surfaced only if it was suggested for or already applied to >= 80% of the selection; coverage counts include images that already have the tag so a near- universal tag isn't penalized for dropping out of suggestion lists. Accept-only chips (no reject) match the rest of the suggestions UX. Frontend polish in the same commit: - Showcase session dedup (exclude seen ids, reset on second lap) and aspect-ratio-aware placeholder heights for better column balancing - Modal touch-swipe navigation on the image wrapper, auto-focus of the tag input on desktop (>=768px), and autocomplete flip-up when the dropdown would cover the Suggestions section - Settings template inline styles replaced with utility classes - Inline series-editor script extracted to gallery-series-editor.js Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,15 @@
|
||||
let columnHeights = [];
|
||||
let isLoading = false;
|
||||
let currentColumnCount = 0;
|
||||
const seenImageIds = new Set();
|
||||
|
||||
function buildRandomUrl(count) {
|
||||
const params = new URLSearchParams({ count: String(count) });
|
||||
if (seenImageIds.size > 0) {
|
||||
params.set('exclude', [...seenImageIds].join(','));
|
||||
}
|
||||
return `/api/random-images?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
@@ -149,6 +158,13 @@
|
||||
const imgEl = document.createElement('img');
|
||||
imgEl.src = img.thumb_url;
|
||||
imgEl.alt = img.filename;
|
||||
// Reserve the correct layout slot before the image loads so the masonry
|
||||
// doesn't reflow when thumbnails arrive. Width/height populate on import
|
||||
// and may be missing on older records — fall back to natural sizing.
|
||||
if (img.width && img.height) {
|
||||
imgEl.width = img.width;
|
||||
imgEl.height = img.height;
|
||||
}
|
||||
if (useLazyLoading) {
|
||||
imgEl.loading = 'lazy';
|
||||
}
|
||||
@@ -173,7 +189,10 @@
|
||||
}
|
||||
|
||||
function distributeImages(images, useLazyLoading = false, animate = false) {
|
||||
const colWidth = columns[0] ? columns[0].clientWidth : 0;
|
||||
|
||||
images.forEach((img, i) => {
|
||||
seenImageIds.add(img.id);
|
||||
const colIndex = getShortestColumnIndex();
|
||||
const item = createImageElement(img, useLazyLoading);
|
||||
|
||||
@@ -186,7 +205,13 @@
|
||||
}
|
||||
|
||||
columns[colIndex].appendChild(item);
|
||||
columnHeights[colIndex] += 300;
|
||||
// Predict rendered height from the intrinsic aspect ratio so masonry
|
||||
// packing is accurate before the thumb loads. Fall back to a flat guess
|
||||
// when W/H are missing.
|
||||
const predictedHeight = (colWidth && img.width && img.height)
|
||||
? colWidth * (img.height / img.width)
|
||||
: 300;
|
||||
columnHeights[colIndex] += predictedHeight;
|
||||
const imgEl = item.querySelector('img');
|
||||
imgEl.onload = () => { updateColumnHeights(); };
|
||||
});
|
||||
@@ -228,10 +253,18 @@
|
||||
container.classList.add('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/random-images?count=20`);
|
||||
let response = await fetch(buildRandomUrl(20));
|
||||
if (!response.ok) throw new Error('Failed to fetch');
|
||||
let data = await response.json();
|
||||
|
||||
const data = await response.json();
|
||||
// If the collection is exhausted for this session, reset and retry once
|
||||
// so shuffle keeps feeling fresh on a "second lap".
|
||||
if (data.images.length === 0 && seenImageIds.size > 0) {
|
||||
seenImageIds.clear();
|
||||
response = await fetch(buildRandomUrl(20));
|
||||
if (!response.ok) throw new Error('Failed to fetch');
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
// Clear everything
|
||||
columns.forEach(col => col.innerHTML = '');
|
||||
@@ -254,7 +287,7 @@
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/random-images?count=${LOAD_BATCH_SIZE}`);
|
||||
const response = await fetch(buildRandomUrl(LOAD_BATCH_SIZE));
|
||||
if (!response.ok) throw new Error('Failed to fetch');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user