const form = document.getElementById('uploadForm'); const input = document.getElementById('imageInput'); const preview = document.getElementById('preview'); const dropZone = document.getElementById('dropZone'); const fileName = document.getElementById('fileName'); const button = document.getElementById('analyseButton'); const title = document.getElementById('verdictTitle'); const badge = document.getElementById('verdictBadge'); const note = document.getElementById('reportNote'); const status = document.getElementById('serviceStatus'); const rows = [...document.querySelectorAll('.test-list li')]; let previewUrl = ''; function setServiceState(online) { status.textContent = online ? 'Service ready' : 'Service unavailable'; status.className = `status ${online ? 'online' : 'offline'}`; } fetch('/api/health', { cache: 'no-store' }) .then((response) => setServiceState(response.ok)) .catch(() => setServiceState(false)); function selectFile(file) { if (!file) return; if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) { showError('Select a JPEG, PNG or WebP image.'); input.value = ''; return; } if (file.size > 8 * 1024 * 1024) { showError('The selected image is larger than 8 MB.'); input.value = ''; return; } if (previewUrl) URL.revokeObjectURL(previewUrl); previewUrl = URL.createObjectURL(file); preview.src = previewUrl; fileName.textContent = file.name; dropZone.classList.add('has-preview'); resetReport(); } input.addEventListener('change', () => selectFile(input.files[0])); for (const eventName of ['dragenter', 'dragover']) { dropZone.addEventListener(eventName, (event) => { event.preventDefault(); dropZone.classList.add('dragging'); }); } for (const eventName of ['dragleave', 'drop']) { dropZone.addEventListener(eventName, (event) => { event.preventDefault(); dropZone.classList.remove('dragging'); }); } dropZone.addEventListener('drop', (event) => { const file = event.dataTransfer.files[0]; if (!file) return; const transfer = new DataTransfer(); transfer.items.add(file); input.files = transfer.files; selectFile(file); }); function resetReport() { title.textContent = 'Ready to analyse'; badge.textContent = 'Not tested'; badge.className = 'verdict neutral'; note.textContent = 'Tests run in order and stop immediately when one rejects the image.'; rows.forEach((row) => { row.className = ''; row.querySelector('b').textContent = 'Waiting'; row.removeAttribute('title'); }); } function showRunning() { title.textContent = 'Analysis in progress'; badge.textContent = 'Running'; badge.className = 'verdict running'; note.textContent = 'Free Spaces can be slow after waking up. Keep this page open.'; rows.forEach((row) => { row.className = ''; row.querySelector('b').textContent = 'Waiting'; row.removeAttribute('title'); }); } function showError(message) { title.textContent = 'Analysis unavailable'; badge.textContent = 'Error'; badge.className = 'verdict error'; note.textContent = message; } function renderReport(report) { const labels = { approved: 'Approved', rejected: 'Rejected', error: 'Error' }; title.textContent = report.verdict === 'approved' ? 'Image passed every test' : report.verdict === 'rejected' ? 'Image did not pass moderation' : 'Analysis could not finish'; badge.textContent = labels[report.verdict] || 'Error'; badge.className = `verdict ${report.verdict}`; rows.forEach((row, index) => { const result = report.results[index]; if (!result) { row.className = 'skipped'; row.querySelector('b').textContent = 'Skipped'; return; } row.className = result.status; const score = typeof result.score === 'number' ? ` · ${(result.score * 100).toFixed(1)}%` : ''; row.querySelector('b').textContent = `${result.status === 'passed' ? 'Passed' : result.status === 'rejected' ? 'Failed' : 'Error'}${score}`; if (result.detail || result.warning) row.title = [result.detail, result.warning].filter(Boolean).join(' '); }); const failed = report.results.find((result) => !result.passed); note.textContent = failed ? `${failed.name} stopped the sequence. ${failed.detail || failed.warning || ''}`.trim() : 'All four automated tests completed successfully.'; } function renderProgress(event) { title.textContent = 'Analysis in progress'; badge.textContent = 'Running'; badge.className = 'verdict running'; const active = typeof event.active === 'number' ? event.active : -1; rows.forEach((row, index) => { const result = event.results && event.results[index]; row.removeAttribute('title'); if (result) { row.className = result.status; const score = typeof result.score === 'number' ? ` · ${(result.score * 100).toFixed(1)}%` : ''; row.querySelector('b').textContent = `${result.status === 'passed' ? 'Passed' : result.status === 'rejected' ? 'Failed' : 'Error'}${score}`; if (result.detail || result.warning) row.title = [result.detail, result.warning].filter(Boolean).join(' '); } else if (index === active) { row.className = 'running'; row.querySelector('b').textContent = 'Running'; } else { row.className = ''; row.querySelector('b').textContent = 'Waiting'; } }); note.textContent = active >= 0 ? `${event.activeName || rows[active]?.children[1]?.textContent || 'Current test'} is running.` : 'Tests run in order and stop immediately when one rejects the image.'; } async function readProgressStream(response) { if (!response.body) { const payload = await response.json(); if (!response.ok) throw new Error(payload.detail || `HTTP error ${response.status}`); renderReport(payload); return; } if (!response.ok) { let payload = {}; try { payload = await response.json(); } catch {} throw new Error(payload.detail || `HTTP error ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.trim()) continue; const event = JSON.parse(line); if (event.type === 'progress') renderProgress(event); if (event.type === 'complete') renderReport(event.report); } } if (buffer.trim()) { const event = JSON.parse(buffer); if (event.type === 'progress') renderProgress(event); if (event.type === 'complete') renderReport(event.report); } } form.addEventListener('submit', async (event) => { event.preventDefault(); const file = input.files[0]; if (!file) { showError('Select an image before running moderation.'); return; } showRunning(); button.disabled = true; button.textContent = 'Analysing...'; const body = new FormData(); body.append('image', file); try { const response = await fetch('/api/analyse-stream', { method: 'POST', body }); await readProgressStream(response); } catch (error) { showError(error.message || 'The analysis request failed.'); } finally { button.disabled = false; button.textContent = 'Run moderation'; } });