/** * Shared library toolbar JS — handles batch generation, tag regeneration, * and bulk create operations for all category index pages. * * Reads configuration from data attributes on the toolbar wrapper element: * data-toolbar-category — e.g. "outfits" * data-get-missing-url — e.g. "/get_missing_outfits" * data-clear-covers-url — e.g. "/clear_all_outfit_covers" * data-generate-url — e.g. "/outfit/{slug}/generate" (with {slug} placeholder) * data-regen-tags-category — e.g. "outfits" (for /admin/bulk_regenerate_tags/) * data-bulk-create-url — e.g. "/outfits/bulk_create" */ document.addEventListener('DOMContentLoaded', () => { const toolbar = document.querySelector('[data-toolbar-category]'); if (!toolbar) return; const category = toolbar.dataset.toolbarCategory; const getMissingUrl = toolbar.dataset.getMissingUrl; const clearCoversUrl = toolbar.dataset.clearCoversUrl; const generateUrlPattern = toolbar.dataset.generateUrl; const regenTagsCat = toolbar.dataset.regenTagsCategory; const bulkCreateUrl = toolbar.dataset.bulkCreateUrl; const batchBtn = document.getElementById('batch-generate-btn'); const regenAllBtn = document.getElementById('regenerate-all-btn'); const regenTagsBtn = document.getElementById('regen-tags-all-btn'); const bulkCreateBtn = document.getElementById('bulk-create-btn'); const bulkOverwriteBtn = document.getElementById('bulk-overwrite-btn'); // --- Utility: poll a job until done --- function waitForJob(jobId) { return new Promise((resolve, reject) => { const poll = setInterval(async () => { try { const resp = await fetch(`/api/queue/${jobId}/status`); const data = await resp.json(); if (data.status === 'done') { clearInterval(poll); resolve(data); } else if (data.status === 'failed' || data.status === 'removed') { clearInterval(poll); reject(new Error(data.error || 'Job failed')); } } catch (err) { /* ignore transient errors */ } }, 1500); }); } // --- Batch Generate Missing Covers --- async function runBatch() { if (!getMissingUrl || !generateUrlPattern) return; const response = await fetch(getMissingUrl); const data = await response.json(); const missing = data.missing; if (missing.length === 0) { alert('No items missing cover images.'); return; } if (batchBtn) batchBtn.disabled = true; if (regenAllBtn) regenAllBtn.disabled = true; // Phase 1: Queue all jobs const jobs = []; for (const item of missing) { try { const url = generateUrlPattern.replace('{slug}', item.slug); const body = new URLSearchParams({ action: 'replace' }); // Secondary categories need a random character for generation if (category !== 'characters') { body.set('character_slug', '__random__'); } const genResp = await fetch(url, { method: 'POST', body: body, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const genData = await genResp.json(); if (genData.job_id) jobs.push({ item, jobId: genData.job_id }); } catch (err) { console.error(`Failed to queue ${item.name}:`, err); } } // Phase 2: Poll all concurrently, update card images as they finish await Promise.all(jobs.map(async ({ item, jobId }) => { try { const jobResult = await waitForJob(jobId); if (jobResult.result && jobResult.result.image_url) { const img = document.getElementById(`img-${item.slug}`); const noImgSpan = document.getElementById(`no-img-${item.slug}`); if (img) { img.src = jobResult.result.image_url; img.classList.remove('d-none'); } if (noImgSpan) noImgSpan.classList.add('d-none'); } } catch (err) { console.error(`Failed for ${item.name}:`, err); } })); if (batchBtn) batchBtn.disabled = false; if (regenAllBtn) regenAllBtn.disabled = false; alert(`Batch generation complete! ${jobs.length} images queued.`); } if (batchBtn) { batchBtn.addEventListener('click', async () => { if (!getMissingUrl) return; const response = await fetch(getMissingUrl); const data = await response.json(); if (data.missing.length === 0) { alert('No items missing cover images.'); return; } if (!confirm(`Generate cover images for ${data.missing.length} items?`)) return; runBatch(); }); } // --- Regenerate All Covers --- if (regenAllBtn) { regenAllBtn.addEventListener('click', async () => { if (!clearCoversUrl) return; if (!confirm('This will unassign ALL current cover images and generate new ones. Proceed?')) return; const clearResp = await fetch(clearCoversUrl, { method: 'POST' }); if (clearResp.ok) { document.querySelectorAll('.img-container img').forEach(img => img.classList.add('d-none')); document.querySelectorAll('.img-container .text-muted').forEach(span => span.classList.remove('d-none')); runBatch(); } }); } // --- Regenerate Tags (LLM) --- if (regenTagsBtn && regenTagsCat) { regenTagsBtn.addEventListener('click', async () => { if (!confirm('Regenerate tags for ALL items using the LLM? This will consume API credits.')) return; regenTagsBtn.disabled = true; const origText = regenTagsBtn.textContent; regenTagsBtn.textContent = 'Queuing...'; try { const resp = await fetch(`/admin/bulk_regenerate_tags/${regenTagsCat}`, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const data = await resp.json(); if (data.success) { alert(`Queued ${data.queued} tag regeneration tasks. Watch progress in the queue.`); } else { alert('Error: ' + (data.error || 'Unknown error')); } } catch (err) { alert('Request failed: ' + err.message); } regenTagsBtn.disabled = false; regenTagsBtn.textContent = origText; }); } // --- Bulk Create from LoRAs (LLM) --- async function doBulkCreate(overwrite) { if (!bulkCreateUrl) return; const body = overwrite ? new URLSearchParams({ overwrite: 'true' }) : undefined; try { const resp = await fetch(bulkCreateUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body, }); const data = await resp.json(); if (data.success) { alert(`Queued ${data.queued} LLM tasks (${data.skipped} skipped). Watch progress in the queue.`); } else { alert('Error: ' + (data.error || 'Unknown error')); } } catch (err) { alert('Request failed: ' + err.message); } } if (bulkCreateBtn) { bulkCreateBtn.addEventListener('click', () => { if (!confirm('Create entries from LoRA files using the LLM? This will consume API credits.')) return; doBulkCreate(false); }); } if (bulkOverwriteBtn) { bulkOverwriteBtn.addEventListener('click', () => { if (!confirm('WARNING: This will overwrite ALL existing metadata using the LLM. This consumes API credits. Proceed?')) return; doBulkCreate(true); }); } });