// The server starts the generation clock after weights and GPU admission. const root = document.getElementById('st-app'); if (!root || root.dataset.generationProgressInstalled) return; root.dataset.generationProgressInstalled = 'true'; let token = ''; let started = null; let elapsedFloor = 0; let lastState = ''; let clock = 'waiting'; let phase = 'queued'; let queueState = 'checking'; let queuePosition = null; let queueSize = null; let queueAhead = null; let queueRunning = null; const text = (element, value) => { if (element && element.textContent !== value) element.textContent = value; }; const paint = () => { const card = root.querySelector('.st-generation'); if (!card) { token = ''; lastState = ''; started = null; return; } if (card.dataset.token !== token) { token = card.dataset.token; started = null; elapsedFloor = 0; lastState = ''; clock = 'waiting'; phase = card.dataset.phase || 'queued'; queueState = 'checking'; queuePosition = queueSize = queueAhead = queueRunning = null; } const state = root.querySelector('#st-generation-state [data-token]'); const signature = state ? [state.dataset.token, state.dataset.status, state.dataset.label, state.dataset.elapsedSeconds, state.dataset.clock, state.dataset.queueState, state.dataset.queuePosition, state.dataset.queueSize, state.dataset.queueAhead, state.dataset.queueRunning].join('|') : ''; if (state && signature !== lastState && state.dataset.token === token) { lastState = signature; phase = state.dataset.status; queueState = state.dataset.queueState || 'checking'; const count = value => value !== undefined && /^\d+$/.test(value) ? Number(value) : null; queuePosition = count(state.dataset.queuePosition); queueSize = count(state.dataset.queueSize); queueAhead = count(state.dataset.queueAhead); queueRunning = count(state.dataset.queueRunning); const nextClock = state.dataset.clock || 'waiting'; const serverElapsed = Number(state.dataset.elapsedSeconds) || 0; if (nextClock === 'running') { // A delayed poll must not rewind a clock that is already running. const localElapsed = started === null ? 0 : (performance.now() - started) / 1000; elapsedFloor = Math.max(serverElapsed, localElapsed); started = performance.now() - elapsedFloor * 1000; } else { elapsedFloor = serverElapsed; started = null; } clock = nextClock; text(card.querySelector('.st-generation-stage'), 'Stage: ' + state.dataset.label); } const preparing = phase === 'preparing'; const timed = clock !== 'waiting'; text(card.querySelector('h3'), preparing ? 'Loading weights' : timed ? 'Generating 3D' : 'Waiting for GPU'); card.querySelector('.st-generation-preparing').hidden = !preparing; card.querySelector('.st-generation-timing').hidden = !timed; card.querySelector('.st-generation-stage').hidden = !timed; card.querySelector('.st-generation-queue').hidden = timed || preparing; let positionText = queueState === 'checking' ? 'Checking queue position…' : 'Queue position is currently unavailable.'; let detailText = ''; if (queueState === 'waiting' && queuePosition > 0 && queueSize >= queuePosition && queueAhead !== null && queueRunning !== null) { positionText = `Your queue position: #${queuePosition} of ${queueSize} waiting`; detailText = `${queueAhead} ${queueAhead === 1 ? 'task' : 'tasks'} waiting ahead · ${queueRunning} running`; } else if (queueState === 'admitted') { positionText = 'Your task has reached the GPU worker.'; detailText = 'Starting…'; } text(card.querySelector('.st-generation-queue-position'), positionText); text(card.querySelector('.st-generation-queue-detail'), detailText); card.classList.toggle('waiting', false); if (!timed) return; const elapsed = started === null ? elapsedFloor : Math.max(elapsedFloor, (performance.now() - started) / 1000); const estimate = Number(card.dataset.estimate) || 360; const percent = Math.min(95, elapsed / estimate * 100); text(card.querySelector('.st-generation-elapsed'), `Elapsed: ${Math.floor(elapsed)} s`); const track = card.querySelector('.st-estimate-track'); track.setAttribute('aria-valuenow', String(Math.floor(percent))); track.setAttribute('aria-valuetext', `Elapsed ${Math.floor(elapsed)} seconds; estimated total ${estimate} seconds`); card.querySelector('.st-estimate-fill').style.width = `${percent}%`; const overdue = elapsed >= estimate; card.querySelector('.st-generation-overdue').hidden = !overdue; card.classList.toggle('waiting', overdue); }; const observer = new MutationObserver(paint); observer.observe(root, {childList: true, subtree: true}); const interval = window.setInterval(paint, 250); paint(); window.addEventListener('pagehide', () => { observer.disconnect(); window.clearInterval(interval); }, {once: true});