Spaces:
Sleeping
Sleeping
| 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'; | |
| } | |
| }); | |