() => { // Progressive enhancement: inference and view controls work without this script. window.groundingConsoleCleanup?.(); const reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); const abort = new AbortController(); const listen = (el, event, fn) => el.addEventListener(event, fn, { signal: abort.signal }); let observer, resize, frame, interval, selected, targetRoot, image, lastSrc, lastDraw; let waiting = false, waitStart = 0, ready = false, watchdog, instanceIndex = 0; let clientPending = false, previousRunId = '', clientNotice; let zoom = 1, panX = 0, panY = 0, drag, moved = false; const $ = s => document.querySelector(s); const svg = (tag, attrs = {}) => { const el = document.createElementNS('http://www.w3.org/2000/svg', tag); Object.entries(attrs).forEach(([name, value]) => el.setAttribute(name, String(value))); return el; }; function removeLayer() { $('#stage-image .focus-layer')?.remove(); $('#stage-image .focus-pixels')?.remove(); $('#canvas-viewport .focus-caption')?.remove(); lastDraw = null; } function clearSelection() { selected = null; instanceIndex = 0; document.querySelectorAll('.target-chip[aria-pressed=true]').forEach(b => b.setAttribute('aria-pressed', 'false')); const clear = $('.clear-focus'); if (clear) clear.hidden = true; removeLayer(); const detail = $('.evidence-detail'); if (detail) detail.hidden = true; } function showDetail(coords) { const detail = $('.evidence-detail'), preview = $('.detail-image'); if (!detail || !preview || !image?.naturalWidth) return; detail.hidden = false; const p = coords[instanceIndex]; const box = p.length === 4 ? p : [Math.max(0,p[0]-65), Math.max(0,p[1]-65), Math.min(999,p[0]+65), Math.min(999,p[1]+65)]; const iw = image.naturalWidth, ih = image.naturalHeight; const cx = (box[0]+box[2])/1998*iw, cy = (box[1]+box[3])/1998*ih; const cw = Math.max(32,(box[2]-box[0])/999*iw*1.3), ch = Math.max(32,(box[3]-box[1])/999*ih*1.3); const scale = Math.min(preview.clientWidth/cw, preview.clientHeight/ch, 8); const source = $('.focus-source'); preview.style.backgroundImage = `url(${JSON.stringify(source?.src || image.currentSrc || image.src)})`; preview.style.backgroundSize = `${iw*scale}px ${ih*scale}px`; preview.style.backgroundPosition = `${preview.clientWidth/2-cx*scale}px ${preview.clientHeight/2-cy*scale}px`; $('.detail-coordinates').textContent = `Instance ${instanceIndex + 1} / ${coords.length} · [${p.join(', ')}]`; } function canvasTransform() { const stage = $('#stage-image'); if (!stage) return; const maxX = stage.clientWidth*(zoom-1)/2, maxY = stage.clientHeight*(zoom-1)/2; panX = Math.max(-maxX,Math.min(maxX,panX)); panY = Math.max(-maxY,Math.min(maxY,panY)); stage.style.transform = `translate(${panX}px,${panY}px) scale(${zoom})`; $('#canvas-viewport')?.classList.toggle('is-zoomed',zoom>1); const value = $('.zoom-value'); if (value) value.textContent = `${Math.round(zoom*100)}%`; const out = $('[data-canvas=out]'), up = $('[data-canvas=in]'); if (out) out.disabled = zoom<=1; if (up) up.disabled = zoom>=4; } function resetCanvas() { zoom=1; panX=panY=0; canvasTransform(); } function drawSelection() { const stage = $('#stage-image'); if (!selected?.isConnected || !image?.complete || !image.naturalWidth || waiting) return; let coords; try { coords = JSON.parse(selected.dataset.coordinates); } catch { clearSelection(); return; } if (!Array.isArray(coords) || !coords.length || !coords.every(p => [2, 4].includes(p.length) && p.every(n => Number.isFinite(n) && n >= 0 && n <= 999))) { clearSelection(); return; } const rect = image.getBoundingClientRect(), outer = stage.getBoundingClientRect(); const matrix = getComputedStyle(stage).transform; const zoomNow = matrix === 'none' ? 1 : new DOMMatrixReadOnly(matrix).a; const scale = Math.min(rect.width / zoomNow / image.naturalWidth, rect.height / zoomNow / image.naturalHeight); const w = image.naturalWidth * scale, h = image.naturalHeight * scale; if (w <= 0 || h <= 0) return; const left = (rect.left - outer.left) / zoomNow + (rect.width / zoomNow - w) / 2; const top = (rect.top - outer.top) / zoomNow + (rect.height / zoomNow - h) / 2; instanceIndex = Math.max(0, Math.min(instanceIndex, coords.length - 1)); const signature = JSON.stringify([selected.dataset.label, instanceIndex, coords, image.currentSrc, left, top, w, h]); if (signature === lastDraw) return; const activeAction = document.activeElement?.closest('.focus-caption button')?.dataset.instance; removeLayer(); lastDraw = signature; const source = $('.focus-source'); if (source) { const pixels = document.createElement('img'); pixels.className = 'focus-pixels'; pixels.alt = ''; pixels.setAttribute('aria-hidden', 'true'); pixels.src = source.src; Object.assign(pixels.style, {left: `${left}px`, top: `${top}px`, width: `${w}px`, height: `${h}px`}); stage.append(pixels); } const layer = svg('svg', { viewBox: `0 0 ${w} ${h}`, 'aria-hidden': 'true', class: 'focus-layer' }); Object.assign(layer.style, { left: `${left}px`, top: `${top}px`, width: `${w}px`, height: `${h}px` }); const defs = svg('defs'), mask = svg('mask', { id: 'grounding-focus-mask', maskUnits: 'userSpaceOnUse', x: 0, y: 0, width: w, height: h }); mask.append(svg('rect', { width: w, height: h, fill: 'white' })); const marks = []; for (const p of [coords[instanceIndex]]) { const attr = p.length === 4 ? { x: p[0] / 999 * w, y: p[1] / 999 * h, width: (p[2] - p[0]) / 999 * w, height: (p[3] - p[1]) / 999 * h, rx: 2 } : { cx: p[0] / 999 * w, cy: p[1] / 999 * h, r: 13 }; const tag = p.length === 4 ? 'rect' : 'circle'; mask.append(svg(tag, { ...attr, fill: 'black' })); const outline = svg(tag, { ...attr, class: 'focus-outline', pathLength: 1 }); outline.style.stroke = getComputedStyle(selected.querySelector('i')).backgroundColor; marks.push(svg(tag, { ...attr, class: 'focus-halo' }), outline); } defs.append(mask); layer.append(defs); layer.append(svg('rect', { width: w, height: h, mask: 'url(#grounding-focus-mask)', class: 'focus-dimmer' }), ...marks); const caption = document.createElement('div'); caption.className = 'focus-caption'; const label = document.createElement('span'); label.setAttribute('role', 'status'); label.setAttribute('aria-live', 'polite'); const evidence = 'Prediction'; label.textContent = `${evidence} · ${selected.dataset.label} · ${instanceIndex + 1} / ${coords.length}`; label.title = label.textContent; caption.append(label); for (const [action, name, path] of [['prev', 'Previous instance', 'm12 5-5 5 5 5'], ['next', 'Next instance', 'm8 5 5 5-5 5']]) { const button = document.createElement('button'); button.type = 'button'; button.dataset.instance = action; button.setAttribute('aria-label', name); button.title = name; button.disabled = coords.length < 2; const icon = svg('svg', {viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true'}); icon.append(svg('path', {d: path, stroke: 'currentColor', 'stroke-width': 1.7})); button.append(icon); caption.append(button); } const close = document.createElement('button'); close.type = 'button'; close.className = 'focus-close'; close.setAttribute('aria-label', 'Close target focus'); const icon = svg('svg', { viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true' }); icon.append(svg('path', { d: 'm6 6 8 8m0-8-8 8', stroke: 'currentColor', 'stroke-width': 1.7, 'stroke-linecap': 'round' })); close.title = 'Close focus and return to targets'; close.append(icon); caption.append(close); stage.append(layer); $('#canvas-viewport').append(caption); if (activeAction) caption.querySelector(`[data-instance=${activeAction}]`)?.focus({preventScroll: true}); showDetail(coords); } function tick() { if (document.hidden || !waiting) return; const clock = $('.wait-clock'); if (clock) clock.textContent = `${((performance.now() - waitStart) / 1000).toFixed(1)} s`; } function setWaiting(value) { if (value === waiting) return; waiting = value; $('#console').setAttribute('aria-busy', String(value)); for (const button of document.querySelectorAll('#run-button,#mobile-run-button')) { button.disabled = value; button.classList.toggle('is-waiting', value); if (value) button.setAttribute('aria-label', 'Grounding in progress'); else button.removeAttribute('aria-label'); } for (const id of ['controls', 'sandbox-panel', 'query-dock', 'upload-button', 'canvas-chrome']) { const el = $(`#${id}`); if (el) el.inert = value; } clearInterval(interval); if (!value) clearTimeout(watchdog); if (value) { clearSelection(); resetCanvas(); waitStart = performance.now(); interval = setInterval(tick, 100); } } function clientStatus(text, phase) { const node = document.createElement('div'); node.className = `stage-status ${phase}`; node.dataset.phase = phase; node.setAttribute('role', 'status'); node.setAttribute('aria-live', 'polite'); const dot = document.createElement('i'); dot.setAttribute('aria-hidden', 'true'); const label = document.createElement('span'); label.textContent = text; node.append(dot, label); if (phase === 'busy') { const clock = document.createElement('time'); clock.className = 'wait-clock'; clock.setAttribute('aria-hidden', 'true'); clock.textContent = '0.0 s'; node.append(clock); } // This node belongs to us, outside Gradio's HTML component. Never replace // Svelte-owned children: doing so can leave an orphan busy status forever. clientNotice?.remove(); clientNotice = node; node.classList.add('client-notice'); $('#stage-toolbar').append(node); $('#stage-toolbar').classList.add('client-pending'); } // A reverse proxy can buffer the first SSE event. Acknowledge the actual click // immediately; backend events still own final results and measured latency. window.groundingConsoleBegin = () => { if (!ready || waiting) return; previousRunId = $('#stage-status .stage-status')?.dataset.runId || ''; clientPending = true; clientStatus('Request submitted · Awaiting inference', 'busy'); setWaiting(true); watchdog = setTimeout(() => { if (!waiting) return; clientStatus('No result received. Check the connection and refresh. The server request has not been cancelled.', 'error'); setWaiting(false); }, 330000); }; function schedule() { if (frame) return; frame = requestAnimationFrame(() => { frame = null; reconcile(); }); } function reconcile() { const stage = $('#stage-image'), targets = $('#targets-content'); if (!stage || !targets) return; const server = $('#stage-status .stage-status'); if (clientPending && (server?.dataset.runId || '') !== previousRunId) { clientPending = false; clientNotice?.remove(); clientNotice = null; $('#stage-toolbar').classList.remove('client-pending'); } if (!clientPending) setWaiting(server?.dataset.phase === 'busy'); const chips = targets.querySelector('.target-chips'); if (targetRoot !== chips) { targetRoot = chips; clearSelection(); } const nextImage = stage.querySelector('img'); if (image !== nextImage) { image = nextImage; if (image) listen(image, 'load', schedule); } const src = image?.currentSrc || image?.src; const download = $('.canvas-download'); if (download && src) download.href = src; if (src !== lastSrc) { clearSelection(); resetCanvas(); lastSrc = src; } const dimensions = $('.image-dimensions'); if (dimensions && image?.naturalWidth) dimensions.textContent = `${image.naturalWidth} × ${image.naturalHeight} px${zoom>1 ? ' / drag to pan' : ''}`; drawSelection(); } function onClick(event) { // The native click has already reached Gradio's button handler. Keep its // State inputs untouched; only enhance presentation during network latency. if (event.target.closest?.('#run-button,#mobile-run-button')) window.groundingConsoleBegin?.(); const instance = event.target.closest?.('[data-instance]')?.dataset.instance; if (instance && selected && !waiting) { const count = JSON.parse(selected.dataset.coordinates).length; instanceIndex = (instanceIndex + (instance === 'next' ? 1 : -1) + count) % count; drawSelection(); return; } const action = event.target.closest?.('[data-canvas]')?.dataset.canvas; if (action && !waiting) { if (action === 'fit') resetCanvas(); else { zoom=Math.max(1,Math.min(4,zoom+(action==='in'?.5:-.5))); canvasTransform(); } schedule(); return; } const chip = event.target.closest?.('#targets-content .target-chip'); if (chip && !waiting) { const wasSelected = chip === selected; clearSelection(); if (!wasSelected) { selected = chip; chip.setAttribute('aria-pressed', 'true'); $('.clear-focus').hidden = false; drawSelection(); const bounds = $('#canvas-viewport').getBoundingClientRect(); if (bounds.top < 0 || bounds.bottom > innerHeight) { $('#canvas-viewport').scrollIntoView({block: 'center', behavior: reduced.matches ? 'instant' : 'smooth'}); $('.focus-close')?.focus({preventScroll: true}); } } } else if (event.target.closest?.('.clear-focus,.focus-close')) { const previous = selected; clearSelection(); previous?.focus(); } // Select an actual returned box directly on the image; no invented geometry. if (!waiting && !moved && event.target === image) { const rect=image.getBoundingClientRect(), scale=Math.min(rect.width/image.naturalWidth,rect.height/image.naturalHeight); const w=image.naturalWidth*scale, h=image.naturalHeight*scale; const x=(event.clientX-rect.left-(rect.width-w)/2)/w*999, y=(event.clientY-rect.top-(rect.height-h)/2)/h*999; const hits=[]; document.querySelectorAll('.target-chip').forEach(chip=>{ let coords; try{coords=JSON.parse(chip.dataset.coordinates);}catch{return;} coords.forEach((p,index)=>{ if (p.length===4 && x>=p[0] && x<=p[2] && y>=p[1] && y<=p[3]) hits.push({chip,index,size:(p[2]-p[0])*(p[3]-p[1])}); if (p.length===2 && Math.hypot(x-p[0],y-p[1])<20) hits.push({chip,index,size:0}); }); }); const hit=hits.sort((a,b)=>a.size-b.size)[0]; if(hit){clearSelection();selected=hit.chip;instanceIndex=hit.index;selected.setAttribute('aria-pressed','true');$('.clear-focus').hidden=false;drawSelection();$('.focus-close')?.focus({preventScroll:true});} } moved=false; } function init() { if (ready || !$('#console') || !$('#targets-content') || !$('#stage-status')) return; ready = true; observer.disconnect(); listen(document, 'click', onClick); listen(document, 'change', schedule); listen(document, 'keydown', e => { if (e.key === 'Escape') {const previous=selected;clearSelection();resetCanvas();previous?.focus();} }); const viewport=$('#canvas-viewport'); listen(viewport,'pointerdown',e=>{ moved=false; if(zoom<=1 || waiting || e.button!==0 || e.target.closest('button') || !e.target.closest('#stage-image')) return; drag={x:e.clientX,y:e.clientY,px:panX,py:panY}; viewport.setPointerCapture(e.pointerId); viewport.classList.add('is-panning'); e.preventDefault(); }); listen(viewport,'pointermove',e=>{ if(!drag)return; const dx=e.clientX-drag.x,dy=e.clientY-drag.y; moved ||= Math.hypot(dx,dy)>4; panX=drag.px+dx;panY=drag.py+dy;canvasTransform(); }); const endDrag=()=>{drag=null;viewport.classList.remove('is-panning');}; listen(viewport,'pointerup',endDrag);listen(viewport,'pointercancel',endDrag); listen(viewport,'dragstart',e=>{if(zoom>1)e.preventDefault();}); listen(document, 'visibilitychange', () => { $('#console')?.classList.toggle('tab-hidden', document.hidden); tick(); }); observer = new MutationObserver(records => { if (records.some(r => !r.target.closest?.('.focus-layer,.focus-caption,.wait-clock'))) schedule(); }); for (const id of ['stage-image', 'targets-content', 'stage-status']) observer.observe($(`#${id}`), { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); resize = new ResizeObserver(schedule); resize.observe($('#stage-image')); resize.observe($('#controls')); reconcile(); document.fonts.ready.then(schedule); } observer = new MutationObserver(init); observer.observe(document.body, { childList: true, subtree: true }); init(); window.groundingConsoleCleanup = () => { abort.abort(); observer?.disconnect(); resize?.disconnect(); cancelAnimationFrame(frame); clearInterval(interval); clearTimeout(watchdog); if (ready) setWaiting(false); removeLayer(); clientNotice?.remove(); $('#stage-toolbar')?.classList.remove('client-pending'); delete window.groundingConsoleBegin; }; }