// Processing + Result modals. // Talks to /gradio_api/* directly via fetch — bypasses @gradio/client (which // hardcodes /gradio/config on hf.space URLs and 404s against gradio.Server). // Auth comes from the HF OAuth session cookie set after /login/huggingface; // same-origin fetches forward it automatically with credentials: 'include'. // Client-side resize: prevents huge phone photos from OOMing ZeroGPU on decode. // Server still resizes to 1024 longest edge; this is an upstream safety net. async function resizeImageFile(file, maxEdge = 2048, quality = 0.92) { if (!/^image\//.test(file.type)) return file; const bitmap = await createImageBitmap(file); const { width: w, height: h } = bitmap; if (Math.max(w, h) <= maxEdge) { bitmap.close?.(); return file; } const scale = maxEdge / Math.max(w, h); const tw = Math.round(w * scale); const th = Math.round(h * scale); const canvas = document.createElement('canvas'); canvas.width = tw; canvas.height = th; canvas.getContext('2d').drawImage(bitmap, 0, 0, tw, th); bitmap.close?.(); const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', quality)); return new File([blob], (file.name || 'upload').replace(/\.[^.]+$/, '') + '.jpg', { type: 'image/jpeg' }); } // Generate a URL-safe session id. Used to key the server-side result store // so the frontend can recover after a tab-away/screen-lock. function newSessionId() { if (window.crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, ''); return Array.from(crypto.getRandomValues(new Uint8Array(16))) .map(b => b.toString(16).padStart(2, '0')).join(''); } function ProcessingModal({ open, file, onDone, onError, onClose }) { const [progress, setProgress] = React.useState(0); const [step, setStep] = React.useState(0); const [imageUrl, setImageUrl] = React.useState(null); const cancelRef = React.useRef({ cancelled: false }); const steps = [ { t: 'Reading the brief', s: 'STEP 1' }, { t: 'Welding the joints', s: 'STEP 2' }, { t: 'Casting in plastic', s: 'STEP 3' }, { t: 'Out of the oven', s: 'STEP 4' }, { t: 'Painting final details', s: 'STEP 5' }, ]; React.useEffect(() => { if (!open || !file) return; cancelRef.current = { cancelled: false }; setProgress(0); setStep(0); const url = URL.createObjectURL(file); setImageUrl(url); // Stamp the URL with a session id so we can recover the result after // a tab-away. Cleared by the App component on modal close. const sessionId = newSessionId(); const params = new URLSearchParams(window.location.search); params.set('session', sessionId); window.history.replaceState(null, '', window.location.pathname + '?' + params.toString()); let p = 0; const id = setInterval(() => { if (cancelRef.current.cancelled) { clearInterval(id); return; } p += 0.6 + Math.random() * 1.2; if (p > 92) p = 92; setProgress(p); setStep(Math.min(4, Math.floor((p / 100) * 5))); }, 120); (async () => { try { const upload = await resizeImageFile(file, 2048, 0.92); // Use @gradio/client (loaded via CDN in index.html). It does the // ZeroGPU handshake — sends `supports-zerogpu-headers: 1` and exchanges // the iframe's __sign for a fresh proxy token. Manual fetch skips this // and the scheduler returns "Expired ZeroGPU proxy token". let tries = 0; while ((!window.__gradio || !window.__gradio.Client) && tries < 100) { await new Promise(r => setTimeout(r, 50)); tries++; } if (!window.__gradio || !window.__gradio.Client) { throw new Error('Gradio client failed to load from CDN'); } const { Client, handle_file } = window.__gradio; const client = await Client.connect(window.location.origin); const result = await client.predict('/action', { image_path: handle_file(upload), session_id: sessionId, }); if (cancelRef.current.cancelled) return; clearInterval(id); setProgress(100); setStep(4); const out = result?.data?.[0]; const outUrl = typeof out === 'string' ? out : (out?.url || out?.path); setTimeout(() => { if (!cancelRef.current.cancelled) onDone(outUrl); }, 400); return; } catch (err) { clearInterval(id); if (!cancelRef.current.cancelled) { console.error('Inference failed:', err); onError && onError(err); } } })(); return () => { cancelRef.current.cancelled = true; clearInterval(id); URL.revokeObjectURL(url); }; }, [open, file]); if (!open) return null; return (