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:
@@ -0,0 +1,83 @@
|
||||
// /app/static/js/gallery-series-editor.js
|
||||
// Series editor wired into the bulk-editor panel on series-filtered gallery views.
|
||||
// No-ops when the page has no series context.
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const seriesInfo = window.galleryState && window.galleryState.seriesInfo;
|
||||
if (!seriesInfo) return;
|
||||
|
||||
const startPageInput = document.getElementById('startPageInput');
|
||||
const bulkAddToSeriesBtn = document.getElementById('bulkAddToSeriesBtn');
|
||||
const feedbackEl = document.getElementById('seriesEditorFeedback');
|
||||
if (!startPageInput || !bulkAddToSeriesBtn || !feedbackEl) return;
|
||||
|
||||
const seriesTagId = seriesInfo.tag_id;
|
||||
|
||||
function updateAddButtonState() {
|
||||
const selectedCount = window.selectedImages ? window.selectedImages.size : 0;
|
||||
bulkAddToSeriesBtn.disabled = selectedCount === 0;
|
||||
bulkAddToSeriesBtn.textContent = selectedCount > 0
|
||||
? `Add ${selectedCount} to Series`
|
||||
: 'Add to Series';
|
||||
}
|
||||
|
||||
document.addEventListener('selectionChanged', updateAddButtonState);
|
||||
|
||||
bulkAddToSeriesBtn.addEventListener('click', async () => {
|
||||
if (!window.selectedImages || window.selectedImages.size === 0) return;
|
||||
|
||||
const imageIds = window.selectionOrder ? [...window.selectionOrder] : Array.from(window.selectedImages);
|
||||
const startPage = parseInt(startPageInput.value) || 1;
|
||||
|
||||
bulkAddToSeriesBtn.disabled = true;
|
||||
bulkAddToSeriesBtn.textContent = 'Adding...';
|
||||
feedbackEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const r = await fetch(`/api/series/${seriesTagId}/pages/bulk-add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image_ids: imageIds, start_page: startPage })
|
||||
});
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
let msg = `Added ${data.added.length} image(s) to series.`;
|
||||
if (data.skipped.length > 0) {
|
||||
msg += ` Skipped ${data.skipped.length}.`;
|
||||
}
|
||||
feedbackEl.innerHTML = `<div class="feedback-success">${msg}</div>`;
|
||||
|
||||
if (data.added.length > 0) {
|
||||
const lastPage = data.added[data.added.length - 1].page_number;
|
||||
startPageInput.value = lastPage + 1;
|
||||
}
|
||||
|
||||
if (window.clearSelection) {
|
||||
window.clearSelection();
|
||||
}
|
||||
|
||||
const pageCountEl = document.getElementById('seriesPageCount');
|
||||
if (pageCountEl) {
|
||||
const currentCount = parseInt(pageCountEl.textContent) || 0;
|
||||
pageCountEl.textContent = `${currentCount + data.added.length} pages`;
|
||||
}
|
||||
|
||||
// "Read Series" button only renders when page_count > 0. If this was the first
|
||||
// batch of pages added, reload so the button appears.
|
||||
const readBtn = document.querySelector('.series-header-actions .primary-btn');
|
||||
if (!readBtn && data.added.length > 0) {
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
feedbackEl.innerHTML = `<div class="feedback-error">${data.error}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
feedbackEl.innerHTML = `<div class="feedback-error">Error: ${e.message}</div>`;
|
||||
} finally {
|
||||
updateAddButtonState();
|
||||
}
|
||||
});
|
||||
|
||||
updateAddButtonState();
|
||||
});
|
||||
Reference in New Issue
Block a user