lsmpp's picture
Show detection and refinement progress in a centered activity card with visible stages and completion feedback
60d86b6 verified
Raw History Blame Contribute Delete
25.2 kB
/* The editor stores original-image coordinates; canvas scaling never changes boxes. */
'use strict';
const $ = (id) => document.getElementById(id);
const state = {session:null, regions:[], selected:null, model:'meanflow', tool:'select', history:[], busy:false, source:null, result:null, stale:false, transport:'http'};
let gesture = null;
const clone = (x) => structuredClone(x);
const escape = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const endpoint = (name) => `/api/sessions/${state.session.id}/${name}`;
const exampleCache = new Map();
function prepareExample(example) {
const cached = exampleCache.get(example);
if(cached && cached.expires > Date.now()) return cached.promise;
const entry = {expires:Date.now()+45*60*1000};
entry.promise = (async()=>{
const session = await api('/api/session',{example});
const image = new Image(); image.src=session.image;
await image.decode();
return {session,image};
})();
exampleCache.set(example,entry);
entry.promise.catch(()=>{if(exampleCache.get(example)===entry)exampleCache.delete(example);});
return entry.promise;
}
async function api(url, body, isForm=false) {
const response = await fetch(url, body === undefined ? {} : {method:'POST', headers:isForm?{}:{'Content-Type':'application/json'}, body:isForm?body:JSON.stringify(body)});
const data = await response.json();
if (!response.ok) throw new Error(typeof data.detail === 'string' ? data.detail : 'Please check the image and region settings.');
return data;
}
function notice(message='', success=false) {
$('notice').textContent=message; $('notice').hidden=!message;
$('notice').classList.toggle('success',success);
if(success)setTimeout(()=>{if($('notice').textContent===message)$('notice').hidden=true;},4500);
}
function pushHistory() {
state.history.push(clone(state.regions));
if(state.history.length>30) state.history.shift();
$('undoButton').disabled=false;
}
function changed() {
if(state.result) {state.stale=true; $('resultSummary').textContent='Regions or settings changed. Refine again to update.'; $('comparePanel').classList.add('result-stale');}
updateControls();
}
function selected() {return state.regions.find(r=>r.id===state.selected);}
let activity = null, activityDismissTimer = null;
function dismissActivity() {
const returnFocus=activity?.status==='error'?$(activity.kind+'Button'):null;
clearTimeout(activityDismissTimer);activityDismissTimer=null;
$('jobProgress').hidden=true;activity=null;returnFocus?.focus();
}
$('closeProgress').onclick=dismissActivity;
function startActivity(kind, message) {
clearTimeout(activityDismissTimer);
activity={kind,status:'running'};
$('jobProgress').hidden=false;$('jobProgress').dataset.state='running';
$('jobTitle').textContent=kind==='detect'?'Detecting text':kind==='refine'?'Refining text':message;
$('closeProgress').hidden=true;
updateActivity(message);
}
function updateActivity(message, progress=null, waiting=true) {
if(!activity||activity.status!=='running')return;
const determinate=!waiting && typeof progress==='number' && Number.isFinite(progress);
const percent=determinate?Math.round(Math.max(0,Math.min(1,progress))*100):null;
if($('jobMessage').textContent!==message)$('jobMessage').textContent=message;
$('jobMessage').hidden=message===$('jobTitle').textContent;
$('jobProgress').classList.toggle('waiting',!determinate);
$('jobPercent').hidden=!determinate;
const label=determinate?`${percent}%`:'';
if($('jobPercent').textContent!==label)$('jobPercent').textContent=label;
$('progressFill').style.width=determinate?`${percent}%`:'0%';
$('progressTrack').setAttribute('aria-valuetext',message);
if(determinate)$('progressTrack').setAttribute('aria-valuenow',String(percent));
else $('progressTrack').removeAttribute('aria-valuenow');
}
function finishActivity(status, message) {
if(!activity)return;
if(status==='done')updateActivity(message,1,false);
activity.status=status;$('jobProgress').dataset.state=status;
$('jobTitle').textContent=status==='error'?'Something went wrong':activity.kind==='detect'?'Text detected':'Text refined';
$('jobMessage').textContent=message;$('closeProgress').hidden=status!=='error';
if(status==='error'){
$('jobProgress').classList.remove('waiting');$('jobPercent').hidden=true;
$('progressTrack').removeAttribute('aria-valuenow');
$('closeProgress').focus();
}else{
const completed=activity;
activityDismissTimer=setTimeout(()=>{if(activity===completed)dismissActivity();},1800);
}
}
function setBusy(busy, message='Preparing', kind='loading') {
state.busy=busy;
document.querySelectorAll('[data-example],[data-model],#uploadButton,#exampleTargets,#resetButton,#drawTool,#undoButton,#targetText,#enableRegion,#deleteRegion,.coords input,.settings input,.region-row input').forEach(e=>e.disabled=busy);
$('regionCanvas').style.pointerEvents=busy?'none':'';
for(const name of ['detect','refine']){const active=busy&&kind===name;$(name+'Button').classList.toggle('is-running',active);$(name+'Button').setAttribute('aria-busy',String(active));}
if(busy)startActivity(kind,message);
else if(activity?.status==='running')dismissActivity();
updateControls();
}
function updateControls() {
const active=state.regions.filter(r=>r.enabled);
$('selectedCount').textContent=`${active.length} selected`;
$('selectedCount').classList.toggle('has-selected',active.length>0);
$('boxCount').textContent=state.regions.length?`${state.regions.length} regions`:'No regions yet';
$('refineButton').disabled=state.busy || !active.length || active.length>24 || active.some(r=>!r.text.trim());
$('detectButton').disabled=state.busy || !state.session;
$('undoButton').disabled=state.busy || !state.history.length;
$('runNote').textContent=state.busy?'You can follow the progress above.':active.length?`${active.length} region${active.length>1?'s':''} · ${state.model==='meanflow'?'MeanFlow':'Teacher'} · ${$('steps').value} steps`:'Select a region and enter its target text.';
$('exampleTargets').hidden=!state.session?.example;
}
function switchView(view) {
if(view==='compare'&&!state.result)return;
$('editorPanel').hidden=view!=='edit'; $('comparePanel').hidden=view!=='compare';
['edit','compare'].forEach(v=>{ $(v+'View').classList.toggle('active',v===view); $(v+'View').setAttribute('aria-selected',String(v===view)); });
}
function applySettings(settings) {
state.model=settings.variant||settings.model||state.model;
document.querySelectorAll('[data-model]').forEach(button=>{
const active=button.dataset.model===state.model;
button.classList.toggle('active',active);button.setAttribute('aria-checked',String(active));
});
for(const [key,id] of [['steps','steps'],['seed','seed'],['guidance','guidance'],['text_threshold','textThreshold']]){
if(settings[key]!=null)$(id).value=settings[key];
}
}
async function loadSession(session, source=null) {
if(!source){source=new Image();source.src=session.image;await source.decode();}
state.session=session;state.regions=clone(session.regions||[]);state.selected=state.regions[0]?.id||null;state.history=[];state.result=null;state.stale=false;state.tool='select';
if(session.settings)applySettings(session.settings);
$('regionResults').hidden=true;$('compareView').disabled=true;
$('regionCanvas').setAttribute('viewBox',`0 0 ${session.width} ${session.height}`);
$('regionCanvas').style.aspectRatio=`${session.width} / ${session.height}`;
const image=$('sourceImage'); image.setAttribute('href',session.image); image.setAttribute('width',session.width);image.setAttribute('height',session.height);
$('imageSize').textContent=`${session.width} × ${session.height}`;
$('wipe').style.aspectRatio=`${session.width} / ${session.height}`;
state.source=source;
document.querySelectorAll('[data-example]').forEach(e=>e.classList.toggle('active',e.dataset.example===session.example));
switchView('edit');setTool('select');render();notice();
}
async function loadExample(example) {
if(state.busy)return;
try{
setBusy(true,'Loading example');
const prepared=await prepareExample(example);
await loadSession(clone(prepared.session),prepared.image);
history.replaceState(null,'',location.pathname);
}catch(e){notice(e.message);}finally{setBusy(false);}
}
async function upload(file) {
if(!file||state.busy)return;
const form=new FormData();form.append('file',file);
try{setBusy(true);await loadSession(await api('/api/upload',form,true));history.replaceState(null,'',location.pathname);}catch(e){notice(e.message);}finally{setBusy(false);$('imageUpload').value='';}
}
function renderBoxes() {
if(!state.session)return;
const scale=state.session.width/650;
$('regionBoxes').innerHTML=state.regions.map((r,i)=>{
const [x,y,x1,y1]=r.bbox;const current=r.id===state.selected;const status=(r.enabled?' enabled':'')+(current?' selected':'');
const size=8*scale, badgeW=23*scale, badgeH=18*scale;
const labelY=Math.max(0,y-badgeH);
const handles=current?[[x,y,'nw'],[x1,y,'ne'],[x,y1,'sw'],[x1,y1,'se']].map(([a,b,c])=>`<rect data-id="${escape(r.id)}" data-handle="${c}" class="resize-handle ${c}" x="${a-size/2}" y="${b-size/2}" width="${size}" height="${size}"/>`).join(''):'';
const label=(r.enabled||current)?`<rect data-id="${escape(r.id)}" class="region-label${status}" x="${x}" y="${labelY}" width="${badgeW}" height="${badgeH}" rx="${3*scale}"/><text class="region-label-text${status}" style="font-size:${11*scale}px" x="${x+badgeW/2}" y="${labelY+badgeH*.72}" text-anchor="middle">${i+1}</text>`:'';
return `<g><rect data-id="${escape(r.id)}" class="region-rect${status}" x="${x}" y="${y}" width="${x1-x}" height="${y1-y}"/>${label}${handles}</g>`;
}).join('');
}
function renderList() {
$('regionsEmpty').hidden=!!state.regions.length;
$('regionList').innerHTML=state.regions.map((r,i)=>`<div class="region-row${r.id===state.selected?' selected':''}${r.enabled?' enabled':''}" data-id="${escape(r.id)}" role="button" tabindex="0" aria-label="Region ${i+1}: ${escape(r.text||'Enter target text')}"><input aria-label="Refine region ${i+1}" type="checkbox" ${r.enabled?'checked':''} ${state.busy?'disabled':''}><span class="region-num">${String(i+1).padStart(2,'0')}</span><span class="region-word">${escape(r.text||'Enter target text')}</span><span class="region-confidence">${r.origin==='example'?'example':r.origin==='manual'?'drawn':r.origin==='saved'?'saved':Math.round((r.confidence||0)*100)+'%'}</span></div>`).join('');
}
function renderDetail() {
const r=selected();$('regionDetail').hidden=!r;if(!r)return;
$('targetText').value=r.text;$('enableRegion').checked=r.enabled;
$('regionOrigin').textContent=r.origin==='ocr'?'OCR proposal':r.origin==='example'?'Example target':r.origin==='saved'?'Previous run':'Drawn by you';
const [x,y,x1,y1]=r.bbox;
$('coordinates').innerHTML=[['X',x],['Y',y],['W',x1-x],['H',y1-y]].map(([label,value],index)=>`<label>${label}<input data-coordinate="${index}" type="number" step="1" min="${index<2?0:1}" value="${Math.round(value)}" aria-label="Region ${label}" ${state.busy?'disabled':''}></label>`).join('');
const crop=$('selectedCrop'); const padding=Math.max(5,(y1-y)*.12);
const left=Math.max(0,x-padding),top=Math.max(0,y-padding),width=Math.min(state.session.width,x1+padding)-left,height=Math.min(state.session.height,y1+padding)-top;
crop.width=Math.max(1,Math.round(width));crop.height=Math.max(1,Math.round(height));
if(state.source?.complete)crop.getContext('2d').drawImage(state.source,left,top,width,height,0,0,crop.width,crop.height);
}
function render() {renderBoxes();renderList();renderDetail();updateControls();}
function selectRegion(id, scroll=false) {
state.selected=id;render();
if(scroll){const row=[...$('regionList').children].find(e=>e.dataset.id===id);row?.scrollIntoView({block:'nearest',behavior:'smooth'});}
$('canvasHint').textContent=id?'Drag the box to move it. Drag a corner to resize it.':'Detect words to get started, or draw a box yourself.';
}
function setTool(tool) {
state.tool=tool;$('selectTool').classList.toggle('active',tool==='select');$('drawTool').classList.toggle('active',tool==='draw');$('regionCanvas').classList.toggle('drawing',tool==='draw');
$('drawHint').hidden=tool!=='draw';
$('canvasHint').textContent=tool==='draw'?'Drag over a word, then enter its target text.':'Click a box to select it. Drag a corner to resize.';
}
function point(event) {
const p=new DOMPoint(event.clientX,event.clientY).matrixTransform($('regionCanvas').getScreenCTM().inverse());
return [Math.max(0,Math.min(state.session.width,p.x)),Math.max(0,Math.min(state.session.height,p.y))];
}
$('regionCanvas').addEventListener('pointerdown',event=>{
if(state.busy||!state.session||event.button!==0)return;
event.preventDefault();const p=point(event),id=event.target.dataset.id;
if(state.tool==='draw') {pushHistory();gesture={mode:'draw',start:p};$('draftBox').removeAttribute('hidden');$('draftBox').setAttribute('width',0);$('draftBox').setAttribute('height',0);}
else if(id){selectRegion(id,true);pushHistory();gesture={mode:event.target.dataset.handle||'move',start:p,box:[...selected().bbox],id};}
else{selectRegion(null);return;}
$('regionCanvas').setPointerCapture(event.pointerId);
});
$('regionCanvas').addEventListener('pointermove',event=>{
if(!gesture)return;const p=point(event),g=gesture;
if(g.mode==='draw'){
const box=[Math.min(g.start[0],p[0]),Math.min(g.start[1],p[1]),Math.max(g.start[0],p[0]),Math.max(g.start[1],p[1])];
const draft=$('draftBox');draft.setAttribute('x',box[0]);draft.setAttribute('y',box[1]);draft.setAttribute('width',box[2]-box[0]);draft.setAttribute('height',box[3]-box[1]);g.box=box;
} else{
const r=state.regions.find(r=>r.id===g.id),[x,y,x1,y1]=g.box;let box;
if(g.mode==='move') {const dx=Math.max(-x,Math.min(state.session.width-x1,p[0]-g.start[0])),dy=Math.max(-y,Math.min(state.session.height-y1,p[1]-g.start[1]));box=[x+dx,y+dy,x1+dx,y1+dy];}
else box=[g.mode.includes('w')?Math.min(p[0],x1-4):x,g.mode.includes('n')?Math.min(p[1],y1-4):y,g.mode.includes('e')?Math.max(p[0],x+4):x1,g.mode.includes('s')?Math.max(p[1],y+4):y1];
r.bbox=box.map(n=>Math.round(n*10)/10);renderBoxes();renderDetail();
}
});
function endGesture(){
if(!gesture)return;const g=gesture;gesture=null;$('draftBox').setAttribute('hidden','');
if(g.mode==='draw'&&g.box&&g.box[2]-g.box[0]>=6&&g.box[3]-g.box[1]>=6){
const r={id:'manual-'+crypto.randomUUID(),bbox:g.box.map(n=>Math.round(n*10)/10),text:'',enabled:true,origin:'manual'};
state.regions.push(r);state.selected=r.id;setTool('select');render();$('targetText').focus();
}else render();changed();
}
$('regionCanvas').addEventListener('pointerup',endGesture);
$('regionCanvas').addEventListener('pointercancel',endGesture);
$('regionList').addEventListener('click',event=>{if(event.target.tagName==='INPUT')return;const row=event.target.closest('[data-id]');if(row)selectRegion(row.dataset.id);});
$('regionList').addEventListener('keydown',event=>{if(event.key==='Enter'){const row=event.target.closest('[data-id]');if(row)selectRegion(row.dataset.id);}});
$('regionList').addEventListener('change',event=>{if(state.busy)return;const row=event.target.closest('[data-id]');if(!row)return;pushHistory();state.regions.find(r=>r.id===row.dataset.id).enabled=event.target.checked;selectRegion(row.dataset.id);changed();});
$('targetText').addEventListener('focus',()=>{if(selected())pushHistory();});
$('targetText').addEventListener('input',()=>{const r=selected();if(!r||state.busy)return;r.text=$('targetText').value;r.enabled=true;$('enableRegion').checked=true;renderList();renderBoxes();changed();});
$('enableRegion').addEventListener('change',()=>{if(!selected()||state.busy)return;pushHistory();selected().enabled=$('enableRegion').checked;render();changed();});
$('coordinates').addEventListener('change',event=>{
if(!selected()||state.busy||!event.target.matches('input'))return;
const values=[...$('coordinates').querySelectorAll('input')].map(e=>Number(e.value));if(values.some(v=>!Number.isFinite(v)))return;
pushHistory();const [a,b,w,h]=values,x=Math.max(0,Math.min(state.session.width-1,a)),y=Math.max(0,Math.min(state.session.height-1,b));selected().bbox=[x,y,Math.min(state.session.width,x+Math.max(1,w)),Math.min(state.session.height,y+Math.max(1,h))];render();changed();
});
function deleteRegion(){if(!selected()||state.busy)return;pushHistory();state.regions=state.regions.filter(r=>r.id!==state.selected);state.selected=null;render();changed();}
$('deleteRegion').onclick=deleteRegion;
$('undoButton').onclick=()=>{if(state.busy||!state.history.length)return;state.regions=state.history.pop();if(!selected())state.selected=null;render();changed();};
$('resetButton').onclick=()=>{if(state.busy||!state.regions.length)return;pushHistory();state.regions=[];state.selected=null;render();changed();};
$('selectTool').onclick=()=>setTool('select');$('drawTool').onclick=()=>setTool(state.tool==='draw'?'select':'draw');
document.addEventListener('keydown',event=>{if(event.key==='Escape'&&activity?.status==='error'){dismissActivity();return;}if(/INPUT|TEXTAREA|SELECT/.test(event.target.tagName)||state.busy)return;if(event.key.toLowerCase()==='d')setTool('draw');if(event.key==='Escape')setTool('select');if(event.key==='Delete'||event.key==='Backspace'){if(selected()){event.preventDefault();deleteRegion();}}});
let gradioClient = null;
async function submitGradio(sid, body) {
if(!gradioClient){
const {Client}=await import('/static/gradio-client.js');
gradioClient=await Client.connect(window.location.origin,{events:['data','status']});
}
let result=null;
const submission=gradioClient.submit('/refine_image',{sid,payload:JSON.stringify(body)});
for await(const event of submission){
if(event.type==='data')result=event.data[0];
if(event.type==='status'){
if(event.stage==='error')throw new Error(event.message||'The GPU request could not finish. Please retry.');
const update=event.progress_data?.at(-1);
const queued=['pending','queued','joining_queue'].includes(event.stage);
updateActivity(update?.desc||(queued?'Waiting for GPU':'Refining'),update?.progress,queued||update?.progress==null);
}
}
if(!result){
let latest=await api(`/api/sessions/${sid}/job`);
for(let attempt=0;attempt<150&&['queued','running'].includes(latest.status);attempt++){
await new Promise(resolve=>setTimeout(resolve,1000));
latest=await api(`/api/sessions/${sid}/jobs/${latest.id}`);
}
if(latest.status==='done')result={job_id:latest.id,result:latest.result};
else throw new Error(latest.message||'The request could not finish. Please retry.');
}
return result;
}
async function runJob(kind) {
if(state.busy||!state.session)return;
const sid=state.session.id;notice();setBusy(true,kind==='detect'?'Preparing detection':'Waiting for GPU',kind);
const body=kind==='detect'?{text_threshold:Number($('textThreshold').value),link_threshold:.2}:{regions:clone(state.regions),variant:state.model,steps:Number($('steps').value),seed:Number($('seed').value),guidance:Number($('guidance').value)};
try{
let job,job_id;
if(kind==='refine'&&state.transport==='gradio'){
const completed=await submitGradio(sid,body);job_id=completed.job_id;
job={status:'done',result:completed.result};
}else{
({job_id}=await api(endpoint(kind),body));
do{
await new Promise(resolve=>setTimeout(resolve,650));
job=await api(`/api/sessions/${sid}/jobs/${job_id}`);
updateActivity(job.message,job.progress,job.status==='queued'||kind==='detect'||job.progress<=0.04);
}while(job.status==='queued'||job.status==='running');
}
if(job.status==='error')throw new Error(job.message);
if(kind==='detect'){
pushHistory();const previous=new Map(state.regions.map(r=>[r.id,r]));
state.regions=job.result.regions.map(r=>previous.get(r.id)||r).concat(state.regions.filter(r=>r.origin==='manual'));
state.selected=state.regions[0]?.id||null;render();changed();switchView('edit');
finishActivity('done',`${job.result.regions.length} regions · ${job.result.elapsed.toFixed(1)}s`);
}else{state.result=job.result;state.stale=false;renderResults();switchView('compare');finishActivity('done',`${job.result.regions.length} regions · ${job.result.elapsed.toFixed(1)}s`);history.replaceState(null,'',`#s=${sid}&j=${job_id}${state.session.example?'&e='+state.session.example:''}`);}
}catch(e){finishActivity('error',e.message);}finally{setBusy(false);renderList();}
}
$('detectButton').onclick=()=>runJob('detect');$('refineButton').onclick=()=>runJob('refine');
$('exampleTargets').onclick=async()=>{if(state.busy||!state.session)return;try{setBusy(true);const data=await api(endpoint('targets'));pushHistory();state.regions=data.regions;state.selected=data.regions[0]?.id||null;applySettings(data.settings);render();changed();switchView('edit');notice();}catch(e){notice(e.message);}finally{setBusy(false);}};
function renderResults(){
const result=state.result,base=endpoint('');$('compareView').disabled=false;$('comparePanel').classList.remove('result-stale');
$('beforeImage').src=state.session.image;$('afterImage').src=base+(result.preview||result.image);
$('downloadImage').href=base+result.image;$('downloadRegions').href=base+result.metadata;
$('resultSummary').textContent=`${result.elapsed.toFixed(1)}s`;
$('resultSummary').title=`${result.model==='meanflow'?'MeanFlow':'Teacher'} · ${result.steps} steps`;
$('regionResults').hidden=false;$('resultsCount').textContent='Before → After';
$('resultCards').innerHTML=result.regions.map((r,i)=>`<article class="result-card"><header><span>${String(i+1).padStart(2,'0')}</span><strong>${escape(r.text)}</strong><button data-crop="${i}" title="Enlarge ${escape(r.text)}">⤢</button></header><div class="crop-pair"><figure><figcaption>BEFORE</figcaption><img src="${base+r.before}" alt="Before: ${escape(r.text)}"></figure><figure><figcaption>AFTER</figcaption><img src="${base+r.after}" alt="After: ${escape(r.text)}"></figure></div></article>`).join('');
}
$('resultCards').addEventListener('click',event=>{const button=event.target.closest('[data-crop]');if(!button)return;const r=state.result.regions[Number(button.dataset.crop)];$('dialogTitle').textContent=r.text;$('dialogBefore').src=endpoint(r.before);$('dialogAfter').src=endpoint(r.after);$('cropDialog').showModal();});
$('closeDialog').onclick=()=>$('cropDialog').close();
$('compareSlider').oninput=()=>$('wipe').style.setProperty('--split',$('compareSlider').value+'%');
$('editView').onclick=()=>switchView('edit');$('compareView').onclick=()=>switchView('compare');
document.querySelectorAll('[data-model]').forEach(button=>button.onclick=()=>{if(state.busy)return;state.model=button.dataset.model;document.querySelectorAll('[data-model]').forEach(b=>{b.classList.toggle('active',b===button);b.setAttribute('aria-checked',String(b===button));});$('steps').value=state.model==='meanflow'?4:25;changed();});
['steps','seed','guidance'].forEach(id=>$(id).onchange=changed);
document.querySelectorAll('[data-example]').forEach(button=>button.onclick=()=>loadExample(button.dataset.example));
$('uploadButton').onclick=()=>$('imageUpload').click();$('imageUpload').onchange=()=>upload($('imageUpload').files[0]);
$('dropZone').addEventListener('dragover',event=>{event.preventDefault();$('dropZone').classList.add('drop-hover');});
$('dropZone').addEventListener('dragleave',()=>$('dropZone').classList.remove('drop-hover'));
$('dropZone').addEventListener('drop',event=>{event.preventDefault();$('dropZone').classList.remove('drop-hover');upload(event.dataTransfer.files[0]);});
async function openStudio(){
try{state.transport=(await api('/api/config')).transport||'http';}catch(e){notice('The studio could not connect. Please refresh the page.');return;}
const params=new URLSearchParams(location.hash.slice(1)),sid=params.get('s'),jid=params.get('j');
if(sid&&jid&&/^[a-f0-9]{32}$/.test(sid)&&/^[a-f0-9]{32}$/.test(jid)){
try{
setBusy(true);
const job=await api(`/api/sessions/${sid}/jobs/${jid}`);
if(job.status!=='done'||job.kind!=='refine')throw new Error('This result is not available.');
const image=new Image();image.src=`/api/sessions/${sid}/preview.webp`;await image.decode();
await loadSession({id:sid,image:image.src,width:image.naturalWidth,height:image.naturalHeight,example:params.get('e')});
state.regions=job.result.regions.map(r=>({...r,enabled:true,origin:'saved'}));state.selected=state.regions[0]?.id||null;
applySettings(job.result);
state.result=job.result;render();renderResults();switchView('compare');return;
}catch(e){notice('This preview has expired. Choose an example to start a new image.');}
finally{setBusy(false);}
}
// Prepare both examples together; switching reuses decoded images and untouched presets.
prepareExample('chinese').catch(()=>{});
await loadExample('english');
}
openStudio();