| const state={runId:null,validationRunId:null,validationPoll:null,centerTab:'active',validationPollErrors:0,validationDetail:null,spaceLinksUnlockedFor:null,bucketName:'space-factory-runs',bucketSource:null,bucketReady:false,poll:null,activeLinks:{},activeRunProgress:null,selectedRunDetail:null,runStatusFilter:'all',runSearch:'',runPage:0,runsPerPage:5,runsCache:[],runsLastLoaded:0,lastRunsRefreshFromPoll:0,lastJobLogPoll:0,pollErrors:0,busy:false,user:null,currentView:'onepage',modelScan:null,runDetailCache:{},runDetailCacheTtlMs:300000}; |
| const TERMINAL_STATUSES=new Set(['done','success','failed','full_inference_success','full_inference_candidate_health_passed','manual_hardware_required','generated_needs_manual_hardware','technical_blocker','health_only','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']); |
| const DEFAULT_TIMELINE=[{label:'Bootstrap',status:'pending'},{label:'Dependencies',status:'pending'},{label:'Auth',status:'pending'},{label:'Model analysis',status:'pending'},{label:'Workspace',status:'pending'},{label:'Node/npm',status:'pending'},{label:'Pi install',status:'pending'},{label:'Pi config',status:'pending'},{label:'Pi run',status:'pending'},{label:'Pi verification',status:'pending'},{label:'Metadata sanitize',status:'pending'},{label:'Requirements sanitize',status:'pending'},{label:'Hardware strategy',status:'pending'},{label:'Create with hardware',status:'pending'},{label:'Create Space',status:'pending'},{label:'Repair pass',status:'pending'},{label:'Upload files',status:'pending'},{label:'Space runtime',status:'pending'},{label:'Space logs',status:'pending'},{label:'API validation',status:'pending'},{label:'Live wait',status:'pending'},{label:'Generation smoke',status:'pending'},{label:'Inference gate',status:'pending'},{label:'Report',status:'pending'},{label:'Done',status:'pending'},{label:'Failure',status:'pending'}]; |
|
|
| function bindOnce(el,eventName,key,handler,options){ |
| if(!el)return; |
| const attr='bound'+String(key||eventName).replace(/[^a-zA-Z0-9]/g,''); |
| if(el.dataset[attr]==='true')return; |
| el.dataset[attr]='true'; |
| el.addEventListener(eventName,handler,options); |
| } |
| function bindAllOnce(selector,eventName,key,handler,options){ |
| document.querySelectorAll(selector).forEach((el,index)=>bindOnce(el,eventName,`${key}${index}`,handler,options)); |
| } |
|
|
| function updateImplementationModeHelp(){ |
| const value=formValue('implementationMode')||'full-inference-gated'; |
| const copy=implementationModeCopy(value); |
| const el=$('implementationModeHelp'); |
| if(!el)return; |
| el.innerHTML=`<strong>${escapeHtml(copy.title)}</strong><span>${escapeHtml(copy.description)}</span>`; |
| } |
|
|
| function saveActiveRun(){try{localStorage.setItem('asf.activeRun',JSON.stringify({runId:state.runId,bucketName:state.bucketName,links:state.activeLinks||{}}))}catch(_){}} |
| function clearSavedRun(){try{localStorage.removeItem('asf.activeRun')}catch(_){} state.spaceLinksUnlockedFor=null} |
| function artifactUrl(runId,bucketSource){if(!runId||!bucketSource)return'';return `https://huggingface.co/buckets/${bucketSource}/tree/runs/${runId}`} |
| function ownerFromBucketSource(bucket){return bucket?String(bucket).split('/')[0]:''} |
| function normalizeLinks(links={}){const out={...links};const bucket=out.bucket_source||state.bucketSource;const run=out.run_id||state.runId;if(!out.artifacts_url&&bucket&&run)out.artifacts_url=artifactUrl(run,bucket);if(!out.job_url){const jobId=out.job_id||out.jobId;const owner=out.job_owner||out.created_by||out.owner||out.username||state.user?.username||ownerFromBucketSource(bucket);if(jobId&&owner)out.job_url=`https://huggingface.co/jobs/${owner}/${jobId}`}return out} |
| function runPassedSpaceCreation(payload={}){ |
| const events=payload.events||payload.view?.activity||payload.detail?.events||[]; |
| const summary=payload.summary||payload.detail?.summary||{}; |
| const stateObj=payload.state||payload.detail?.state||{}; |
| const success=String(payload.status||summary.status||stateObj.status||'').toLowerCase(); |
| if(success.includes('success')||success.includes('succeed')||success==='done'||success==='completed')return true; |
| const passedSteps=new Set(['create_space','upload_files','space_runtime','space_logs','api_validation','live_wait','generation_smoke','inference_gate','report_write','done']); |
| return Array.isArray(events)&&events.some(e=>passedSteps.has(String(e.step||''))&&String(e.status||'').toLowerCase()!=='started'); |
| } |
|
|
| function isValidationRunDetail(detail={}){ |
| const summary=detail.summary||{}; |
| const launch=detail.launch||{}; |
| const stateObj=detail.state||{}; |
| const view=detail.view||{}; |
| const values=[detail.kind,summary.kind,launch.kind,stateObj.kind,view.kind,summary.run_type,launch.run_type,stateObj.run_type,detail.run_type]; |
| return values.some(v=>String(v||'').toLowerCase().includes('validate_existing_space')||String(v||'').toLowerCase().includes('validation')); |
| } |
| function validationTimelineItems(detail={}){ |
| const events=detail.events||detail.view?.activity||[]; |
| if(Array.isArray(detail.timeline)&&detail.timeline.length)return detail.timeline; |
| if(Array.isArray(detail.progress?.timeline)&&detail.progress.timeline.length)return detail.progress.timeline; |
| if(!Array.isArray(events)||!events.length)return [ |
| {step:'bootstrap',label:'Bootstrap',status:'pending'}, |
| {step:'dependencies',label:'Dependencies',status:'pending'}, |
| {step:'auth',label:'Auth',status:'pending'}, |
| {step:'live_wait',label:'Live wait',status:'pending'}, |
| {step:'generation_smoke',label:'Generation smoke',status:'pending'}, |
| {step:'report_write',label:'Report',status:'pending'}, |
| ]; |
| const latest=new Map(); |
| events.forEach(e=>{if(e&&e.step)latest.set(String(e.step),e)}); |
| return Array.from(latest.values()).map(e=>({step:e.step,label:String(e.step||'').replace(/_/g,' '),status:e.status||'running',message:e.message||''})); |
| } |
| function validationProgressPercent(detail={}){ |
| const explicit=Number(detail.progress||detail.percent||detail.summary?.progress); |
| if(Number.isFinite(explicit)&&explicit>0)return Math.max(0,Math.min(100,explicit)); |
| const status=String(detail.status||detail.view?.header?.status||detail.state?.status||detail.summary?.status||'').toLowerCase(); |
| if(TERMINAL_STATUSES.has(status)||status.includes('success')||status.includes('succeed'))return 100; |
| const items=validationTimelineItems(detail); |
| const order=['bootstrap','dependencies','auth','live_wait','api_validation','generation_smoke','inference_gate','report_write','done']; |
| let score=0; |
| items.forEach(item=>{ |
| const idx=order.indexOf(String(item.step||'')); |
| const st=String(item.status||'').toLowerCase(); |
| if(idx>=0&&(st.includes('success')||st==='done'||st==='completed'))score=Math.max(score,idx+1); |
| if(idx>=0&&(st.includes('running')||st.includes('started')||st.includes('waiting')||st.includes('pending')))score=Math.max(score,idx+0.35); |
| }); |
| return Math.max(0,Math.min(98,Math.round((score/order.length)*100))); |
| } |
| function uniqueStrings(values=[]){const out=[];for(const value of values){const text=String(value||'').trim();if(text&&!out.includes(text))out.push(text)}return out} |
| function validationEndpointNames(detail={}){ |
| const smoke=detail.generation_smoke||detail.summary?.generation_smoke||detail.state?.generation_smoke||{}; |
| const schema=detail.api_schema||detail.tests?.api_schema||{}; |
| const names=[]; |
| for(const source of [schema.api_names,schema.discovered_api_names,smoke.discovered_api_names,smoke.api_names]){ |
| if(Array.isArray(source))names.push(...source); |
| } |
| if(smoke.api_name)names.push(smoke.api_name); |
| if(schema.selected_api_name)names.push(schema.selected_api_name); |
| const nested=schema.schema&&typeof schema.schema==='object'?schema.schema:{}; |
| for(const source of [nested.api_names,nested.named_endpoints]){ |
| if(Array.isArray(source))names.push(...source); |
| } |
| return uniqueStrings(names.map(normalizeApiName)); |
| } |
| function endpointSchemaEntriesFromApiSchema(schema={}){ |
| const raw=schema.schema&&typeof schema.schema==='object'?schema.schema:schema; |
| const named=raw.named_endpoints||raw.endpoints||raw.dependencies||[]; |
| if(Array.isArray(named))return named; |
| if(named&&typeof named==='object')return Object.entries(named).map(([api_name,value])=>({api_name,...(value&&typeof value==='object'?value:{})})); |
| return []; |
| } |
| function endpointNameFromEntry(entry={}){ |
| return normalizeApiName(entry.api_name||entry.api_name_for_predict||entry.name||entry.endpoint||entry.path||entry.id||''); |
| } |
| function validationEndpointParameters(detail={},endpointName=''){ |
| const smoke=detail.generation_smoke||detail.summary?.generation_smoke||detail.state?.generation_smoke||{}; |
| const schema=detail.api_schema||detail.tests?.api_schema||{}; |
| const selected=normalizeApiName(endpointName||smoke.api_name||schema.selected_api_name||formValue('validateApi')||''); |
| const entries=endpointSchemaEntriesFromApiSchema(schema); |
| const match=entries.find(entry=>endpointNameFromEntry(entry)===selected); |
| const possible=match?.parameters||match?.inputs||match?.input_components||match?.components||smoke.endpoint_parameters||schema.endpoint_parameters||schema.schema?.endpoint_parameters||[]; |
| const params=Array.isArray(possible)?possible.map(item=>typeof item==='string'?item:(item?.parameter_name||item?.label||item?.name||item?.id||item?.component||'')):[]; |
| return uniqueStrings(params); |
| } |
| function applyValidationEndpoint(endpointName,detail={}){ |
| const name=normalizeApiName(endpointName); |
| setInputValue('validateApi',name); |
| const input=$('validateApi'); |
| if(input){input.dispatchEvent(new Event('input',{bubbles:true}));input.focus({preventScroll:true})} |
| renderValidationEndpoints(detail||state.validationDetail||{}); |
| showMessage(`API name set to ${name}.`,'success'); |
| } |
| function bindValidationEndpointPicker(root){ |
| if(!root||root.dataset.boundEndpointPicker==='true')return; |
| root.dataset.boundEndpointPicker='true'; |
| root.addEventListener('click',e=>{ |
| const btn=e.target.closest('[data-endpoint-name]'); |
| if(!btn)return; |
| e.preventDefault(); |
| applyValidationEndpoint(btn.dataset.endpointName,state.validationDetail||{}); |
| }); |
| } |
| function endpointInputItemsFromEntry(entry={}){ |
| const raw=entry.parameters||entry.inputs||entry.input_components||entry.components||[]; |
| if(!Array.isArray(raw))return []; |
| return raw.map((item,index)=>{ |
| if(typeof item==='string')return {name:item,type:'',component:''}; |
| const name=item.parameter_name||item.label||item.name||item.id||item.component||`arg${index}`; |
| const type=item.type||item.python_type||item.datatype||item.api_info?.type||item.component_type||''; |
| const component=item.component||item.component_type||item.type_name||item.label||''; |
| const optional=item.optional===true||item.required===false; |
| return {name:String(name||''),type:String(type||''),component:String(component||''),optional}; |
| }).filter(x=>x.name); |
| } |
| function endpointOutputItemsFromEntry(entry={}){ |
| const raw=entry.returns||entry.outputs||entry.output_components||[]; |
| if(!Array.isArray(raw))return []; |
| return raw.map((item,index)=>{ |
| if(typeof item==='string')return {name:item,type:'',component:''}; |
| return {name:String(item.label||item.name||item.id||`output${index}`),type:String(item.type||item.python_type||item.datatype||''),component:String(item.component||item.component_type||item.type_name||'')}; |
| }).filter(x=>x.name); |
| } |
| function endpointDetails(detail={},endpointName=''){ |
| const schema=detail.api_schema||detail.tests?.api_schema||{}; |
| const entries=endpointSchemaEntriesFromApiSchema(schema); |
| const selected=normalizeApiName(endpointName||formValue('validateApi')||detail.generation_smoke?.api_name||schema.selected_api_name||''); |
| const match=entries.find(entry=>endpointNameFromEntry(entry)===selected)||{}; |
| const inputs=endpointInputItemsFromEntry(match); |
| const fallbackParams=validationEndpointParameters(detail,selected).map(name=>({name,type:'',component:''})); |
| return { |
| name:selected, |
| description:match.description||match.documentation||match.doc||'', |
| inputs:inputs.length?inputs:fallbackParams, |
| outputs:endpointOutputItemsFromEntry(match), |
| raw:match, |
| }; |
| } |
| function renderEndpointParamChips(items=[]){ |
| if(!items.length)return '<span class="muted">No parameters discovered yet.</span>'; |
| return items.slice(0,10).map(item=>{ |
| const detail=[item.type,item.component,item.optional?'optional':''].filter(Boolean).join(' · '); |
| return `<em title="${escapeHtml(detail||item.name)}"><strong>${escapeHtml(item.name)}</strong>${detail?`<span>${escapeHtml(detail)}</span>`:''}</em>`; |
| }).join('')+(items.length>10?'<em><strong>…</strong></em>':''); |
| } |
| function renderValidationEndpoints(detail={}){ |
| const root=$('spaceTestEndpoints'); |
| if(!root)return; |
| bindValidationEndpointPicker(root); |
| const names=validationEndpointNames(detail); |
| const selected=normalizeApiName(formValue('validateApi')||detail.generation_smoke?.api_name||detail.api_schema?.selected_api_name||names[0]||''); |
| if(!names.length){root.innerHTML='<div class="empty compact-empty">No Gradio endpoints discovered yet. They will appear after the live API schema step.</div>';return} |
| const chips=names.slice(0,12).map(name=>`<button class="endpoint-chip ${name===selected?'selected':''}" type="button" data-endpoint-name="${escapeHtml(name)}" title="Use ${escapeHtml(name)} for validation"><span>${escapeHtml(name)}</span>${name===selected?'<em>selected</em>':''}</button>`).join(''); |
| const details=endpointDetails(detail,selected); |
| const signature=JSON.stringify({names:names.slice(0,12),selected,inputs:details.inputs,outputs:details.outputs,description:details.description||''}); |
| if(root.dataset.signature===signature)return; |
| root.dataset.signature=signature; |
| const inputLine=`<div class="endpoint-io"><strong>Inputs</strong><span>${renderEndpointParamChips(details.inputs)}</span></div>`; |
| const outputLine=details.outputs.length?`<div class="endpoint-io"><strong>Outputs</strong><span>${renderEndpointParamChips(details.outputs)}</span></div>`:''; |
| root.innerHTML=`<div class="endpoint-chip-list">${chips}</div><div class="endpoint-info-card">${inputLine}${outputLine}${details.description?`<p>${escapeHtml(details.description)}</p>`:''}</div>`; |
| } |
| function setGeneratedSpaceLinksEnabled(links={},enabled=false){ |
| const space=enabled?links.target_space_url:''; |
| const settings=enabled?(links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings'):''; |
| for(const id of ['activeOpenSpace','openSpace','launchOpenSpace'])setLink(id,space); |
| for(const id of ['activeOpenSettings','openSettings','manualOpenSettings','launchOpenSettings'])setLink(id,settings); |
| } |
| function setQuickLinks(links={},context={}){links=normalizeLinks(links||{});state.activeLinks=links;for(const id of ['activeOpenJob','openJob','launchOpenJob'])setLink(id,links.job_url);const runForLinks=links.run_id||context.run_id||context.summary?.run_id||state.runId;let spaceReady=runPassedSpaceCreation(context);if(spaceReady&&links.target_space_url)state.spaceLinksUnlockedFor=runForLinks;if(!spaceReady&&state.spaceLinksUnlockedFor&&state.spaceLinksUnlockedFor===runForLinks&&links.target_space_url)spaceReady=true;setGeneratedSpaceLinksEnabled(links,spaceReady);for(const id of ['activeOpenArtifacts','openArtifacts','launchOpenArtifacts'])setLink(id,links.artifacts_url);renderManualAction({links:{...links,target_space_url:spaceReady?links.target_space_url:'',target_space_settings_url:spaceReady?links.target_space_settings_url:''}})} |
| let operationTimer=null;function toastDuration(type='info'){if(type==='loading')return 12000;if(type==='error')return 7000;if(type==='warning'||type==='warn')return 6000;return 3500} |
| function setOperation(message,type='info',opts={}){const el=$('operationStatus');if(!el)return;if(operationTimer){clearTimeout(operationTimer);operationTimer=null}if(!message){el.hidden=true;el.textContent='';el.className='operation-status floating-toast';return}el.hidden=false;el.className='operation-status floating-toast '+type;el.textContent=message;const duration=opts.duration??toastDuration(type);if(duration>0)operationTimer=setTimeout(()=>{setOperation('')},duration)} |
| function showMessage(msg,isError=false){const type=isError===true?'error':(typeof isError==='string'?isError:'info');setOperation(msg,type);const el=$('eventsPanel');if(el&&!state.runId)el.textContent=(isError?'Error: ':'')+msg} |
| function setBusy(isBusy,label='Working…'){state.busy=Boolean(isBusy);if(!state.busy&&$('operationStatus')?.className.includes('loading'))setOperation('');document.querySelectorAll('[data-action-button]').forEach(btn=>{btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady});if($('launchBuild'))setBuildLaunchEnabled();if($('launchValidate'))$('launchValidate').disabled=state.busy;if(state.busy)setOperation(label,'loading')} |
| function setButtonBusy(id,isBusy,busyLabel){const btn=$(id);if(!btn)return;if(isBusy){btn.dataset.oldText=btn.textContent;btn.textContent=busyLabel||'Working…';btn.disabled=true;btn.classList.add('is-busy')}else{if(btn.dataset.oldText)btn.textContent=btn.dataset.oldText;if(id==='launchBuild')setBuildLaunchEnabled();else btn.disabled=state.busy||btn.dataset.requiresBucket==='true'&&!state.bucketReady;btn.classList.remove('is-busy')}} |
| function setBucketFeedback(mode='checking',message='Checking private bucket access…'){ |
| const orb=$('bucketInlineOrb'); |
| if(orb)orb.className='bucket-status-orb '+mode; |
| setText('bucketFeedbackText',message); |
| } |
| function setBucketReady(ok,message){state.bucketReady=Boolean(ok);setBuildLaunchEnabled();const feedback=state.bucketReady?'Ready for builds, validations and artifacts.':(message||'Create your private run bucket before launching builds.');setText('buildGateText',state.bucketReady?'Bucket ready. You can launch builds.':feedback);setText('bucketInlineStatus',state.bucketReady?'Ready':'Setup needed');setBucketFeedback(state.bucketReady?'ready':'error',feedback);for(const id of ['bucketInlineDot','sidebarBucketDot']){const dot=$(id);if(dot)dot.className='status-dot '+(state.bucketReady?'green':'amber-dot')}const inline=$('bucketInlineStatus');if(inline)inline.className='badge '+(state.bucketReady?'success':'warn');document.querySelectorAll('[data-requires-bucket="true"]').forEach(btn=>btn.disabled=state.busy||!state.bucketReady);setBuildLaunchEnabled()} |
| function effectivePreferredHardware(){const preferred=formValue('preferredHardware')||'zero-a10g';const fallback=formValue('fallbackHardware')||'a10g-large';const tryZero=$('tryZeroGpu')?.checked??true;if(tryZero)return preferred;if(preferred==='zero-a10g'||preferred.startsWith('zero'))return fallback;return preferred} |
| function hardwarePlanLabel(){const tryZero=$('tryZeroGpu')?.checked??true;const preferred=effectivePreferredHardware();const fallback=formValue('fallbackHardware')||'a10g-large';const allow=$('allowFallback')?.checked??true;if(tryZero)return `ZeroGPU → ${allow?fallback:'no fixed fallback'}`;return `${preferred}${allow&&fallback!==preferred?' → '+fallback+' fallback':''}`} |
|
|
| function normalizeModelInput(value){return String(value||'').trim().replace('https://huggingface.co/','').split('?')[0].split('#')[0].replace(/^\/+|\/+$/g,'')} |
| function modelScanMatchesCurrent(){return state.modelScan?.ok&&normalizeModelInput(state.modelScan.model_id)===normalizeModelInput(formValue('modelId'))} |
| function scanVerdictClass(verdict){verdict=String(verdict||'').toLowerCase();if(verdict==='safe')return'success';if(verdict==='caution')return'warn';if(verdict==='risky'||verdict==='unsupported')return'error';if(verdict==='scanning')return'running';return'neutral'} |
| function outputTypeFromModelScan(scan={}){ |
| const value=String(scan.expected_output_type||scan.metadata?.expected_output_type||'').toLowerCase(); |
| return ['text','image','video','audio','any'].includes(value)?value:''; |
| } |
| function syncExpectedOutputFromModelScan(scan={}){ |
| const inferred=outputTypeFromModelScan(scan); |
| const select=$('buildExpectedOutput'); |
| if(!inferred||!select)return false; |
| const previous=select.value; |
| select.value=inferred; |
| select.dataset.prescanOutput=inferred; |
| select.dataset.prescanPreviousOutput=previous||''; |
| select.dispatchEvent(new Event('change',{bubbles:true})); |
| return previous!==inferred; |
| } |
| function renderModelPreScan(scan=null){ |
| const card=$('modelPreScanCard'),badge=$('modelPreScanBadge'),signals=$('modelPreScanSignals'),ackWrap=$('modelRiskAckWrap'); |
| if(!card)return; |
| const current=normalizeModelInput(formValue('modelId')); |
| const matches=scan&&scan.ok&&normalizeModelInput(scan.model_id)===current; |
| if(!scan||!matches){ |
| card.className='model-prescan-card required'; |
| setText('modelPreScanTitle','Pre-scan required'); |
| setText('modelPreScanSummary','Run a fast metadata scan before launching a paid Job. It checks the model card guidance, runnable examples, Diffusers signals, files, safetensors, gated access and custom-code risk without downloading weights.'); |
| if(badge){badge.textContent='Required';badge.className='badge neutral'} |
| if(signals){signals.hidden=true;signals.innerHTML=''} |
| if(ackWrap)ackWrap.hidden=true; |
| return; |
| } |
| const verdict=String(scan.verdict||'caution').toLowerCase(); |
| const cls=scanVerdictClass(verdict); |
| card.className='model-prescan-card '+cls; |
| setText('modelPreScanTitle',`${verdict.charAt(0).toUpperCase()+verdict.slice(1)} · score ${scan.score ?? '—'}/100`); |
| setText('modelPreScanSummary',scan.summary||'Model metadata scan completed.'); |
| if(badge){badge.textContent=verdict;badge.className='badge '+cls} |
| const good=(scan.good_signals||[]).slice(0,3); |
| const risk=(scan.risk_signals||[]).slice(0,4); |
| const outputType=outputTypeFromModelScan(scan); |
| const recs=[...(scan.recommendations||[]).slice(0,2)]; |
| if(outputType)recs.unshift(`Expected output: ${outputType}`); |
| if(signals){ |
| signals.hidden=false; |
| signals.innerHTML=`${good.length?`<div><strong>Good</strong>${good.map(x=>`<span class="good">${escapeHtml(x)}</span>`).join('')}</div>`:''}${risk.length?`<div><strong>Watch</strong>${risk.map(x=>`<span class="risk">${escapeHtml(x)}</span>`).join('')}</div>`:''}${recs.length?`<div><strong>Recommendation</strong>${recs.map(x=>`<span>${escapeHtml(x)}</span>`).join('')}</div>`:''}`; |
| } |
| if(ackWrap)ackWrap.hidden=!(verdict==='risky'); |
| } |
| function resetModelPreScan(){state.modelScan=null;const ack=$('modelRiskAck');if(ack)ack.checked=false;renderModelPreScan(null);setBuildLaunchEnabled()} |
| function setBuildLaunchEnabled(){const btn=$('launchBuild');const err=validateBuildForm();if(btn)btn.disabled=state.busy||Boolean(err);if($('buildGateText'))setText('buildGateText',err||'Bucket and model pre-scan ready. You can launch builds.')} |
| async function scanModel(){ |
| const model=formValue('modelId'); |
| if(!model){showMessage('Enter a model card URL or model ID before scanning.',true);return} |
| setButtonBusy('scanModel',true,'Scanning…'); |
| const badge=$('modelPreScanBadge');if(badge){badge.textContent='Scanning';badge.className='badge running'} |
| setText('modelPreScanTitle','Scanning model metadata…'); |
| setText('modelPreScanSummary','Reading the model card, Hub metadata and small config files. We do not download model weights.'); |
| try{ |
| const result=await apiPost('/api/models/pre-scan',{model_id:model}); |
| state.modelScan=result; |
| syncExpectedOutputFromModelScan(result); |
| const ack=$('modelRiskAck');if(ack)ack.checked=false; |
| renderModelPreScan(result); |
| setBuildLaunchEnabled(); |
| showMessage(`Model pre-scan: ${result.verdict} (${result.score}/100)`,result.verdict==='safe'?'success':(result.verdict==='unsupported'||result.verdict==='risky'?true:'warning')); |
| }catch(e){state.modelScan=null;renderModelPreScan(null);showMessage(e.message,true)} |
| finally{setButtonBusy('scanModel',false);setBuildLaunchEnabled()} |
| } |
|
|
| function validateBuildForm(){if(!state.bucketReady)return'Create or check your private run bucket before launching a build.';if(!formValue('modelId'))return'Enter a model card URL or model ID.';if(!formValue('piModel'))return'Enter the Pi model to use.';if(!modelScanMatchesCurrent())return'Run the model pre-scan before launching this build.';const verdict=String(state.modelScan?.verdict||'').toLowerCase();if(verdict==='unsupported')return'This model pre-scan marked the repo as unsupported for automatic build.';if(verdict==='risky'&&!$('modelRiskAck')?.checked)return'Review the model pre-scan risk signals and confirm before launching.';return''} |
| function parseJsonField(id,fallback){const raw=formValue(id);if(!raw)return fallback;return JSON.parse(raw)} |
| function prettyJson(value){return JSON.stringify(value,null,2)} |
| function validateValidationForm(){if(!formValue('validateSpace'))return'Enter the target Space ID to validate.';try{const args=parseJsonField('testArgs',[]);const kwargs=parseJsonField('testKwargs',{});if(!Array.isArray(args))return'Test args must be a JSON array because they are passed as positional arguments to gradio_client.predict(...).';if(!kwargs||Array.isArray(kwargs)||typeof kwargs!=='object')return'Test kwargs must be a JSON object because they are passed as keyword arguments.'}catch(e){return`Invalid JSON payload: ${e.message}`}return''} |
| function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form launches new builds. Existing runs can be inspected in the Runs Explorer on the right.</p>'}const launch=$('launchResultPanel');if(launch)launch.hidden=true;const status=$('progressStatus');if(status){status.textContent='Idle';status.className='badge neutral'}setText('progressTitle','No run selected')} |
| function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel){panel.classList.add('inspecting');panel.innerHTML=`<strong>Inspecting selected run</strong><p>You are viewing run <code>${escapeHtml(runId||'')}</code>. The build form is unchanged and the active run panel is now tracking it.</p>`}} |
| function resetRunDetails(){ |
| const cancel=$('cancelRun');if(cancel)cancel.disabled=true; |
| if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;if(typeof resetProgressStability==='function')resetProgressStability(null); |
| if(typeof activityFeedState!=='undefined'){activityFeedState.lastItems=[];activityFeedState.runId=null} |
| for(const [id,value] of [ |
| ['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTraces','—'], |
| ['homeRunDetailStatus','No run'],['selectedModelId','—'],['progressTitle','No run selected'],['progressStatus','Idle'], |
| ['currentStep','—'],['lastEvent','Select a run from the explorer or launch a new build.'],['elapsed','—'],['lastPolled','—'], |
| ['progressPercent','0%'] |
| ])setText(id,value); |
| const status=$('progressStatus');if(status)status.className='badge neutral'; |
| const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump')} |
| setQuickLinks({}); |
| const full=$('reportPreview');if(full)full.textContent='No report selected.'; |
| const manual=$('manualActionPanel');if(manual)manual.hidden=true; |
| |
| const activity=$('activityFeed');if(activity){activity.dataset.signature='';activity.classList.remove('show-all');activity.innerHTML='<div class="empty">No activity yet.</div>'} |
| |
| const events=$('eventsPanel');if(events)events.textContent='No events yet.'; |
| const artifacts=$('artifactList');if(artifacts)artifacts.textContent='No artifacts indexed yet.'; |
| const recovery=$('agentRecoveryCard');if(recovery){recovery.hidden=true;const body=$('agentRecoveryBody');if(body)body.innerHTML='';const badge=$('agentRecoveryBadge');if(badge){badge.textContent='Not needed';badge.className='badge neutral'}} |
| const docs=$('runDocumentsDock');if(docs)docs.innerHTML='<div class="empty">Run documents will appear as files are written to the bucket.</div>'; |
| const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''} |
| for(const id of ['deleteActiveRun','activePrefillSpaceTest','deleteValidationRun']){const btn=$(id);if(btn)btn.disabled=true} |
| renderTimeline(DEFAULT_TIMELINE,'idle'); |
| } function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.activeRunProgress=null;state.selectedRunDetail=null;state.activeLinks={};state.pollErrors=0;clearSavedRun();document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','—');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')} |
| function setActiveRun(result){if(state.poll){clearInterval(state.poll);state.poll=null}resetRunDetails();state.runId=result.run_id;state.spaceLinksUnlockedFor=null;if(Array.isArray(state.runsCache)&&typeof sortRunsNewestFirst==='function'){state.runsCache=sortRunsNewestFirst([{...result,run_id:result.run_id,status:result.status||'running',created_at:result.created_at||new Date().toISOString()},...state.runsCache.filter(r=>r.run_id!==result.run_id)]);renderRunsFromCache();}if(result.bucket_source)state.bucketSource=result.bucket_source;setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'—');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'—');renderActiveRunMeta({summary:result,state:result,launch:result});renderProgress({run_id:result.run_id,status:result.status||'running',progress:1,current_step_label:'Launching build Job…',events:[{step:'launch',status:'started',message:'Build Job launched; waiting for first worker event.'}],summary:result,state:result});setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username,target_space_url:result.target_space_url},result);saveActiveRun();startPolling(result.run_id)} |
| function startPolling(runId,options={}){ |
| if(!runId)return; |
| const sameRun=state.runId===runId; |
| if(sameRun&&state.poll)return; |
| if(!sameRun){ |
| state.spaceLinksUnlockedFor=null; |
| const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''} |
| } |
| state.runId=runId; |
| state.pollErrors=0; |
| state.lastJobLogPoll=0; |
| if(typeof resetProgressStability==='function')resetProgressStability(runId); |
| if(state.poll)clearInterval(state.poll); |
| saveActiveRun(); |
| if(!options.deferFirstTick)refreshProgress(); |
| else setTimeout(()=>{if(state.runId===runId)refreshProgress()},80); |
| state.poll=setInterval(refreshProgress,2000); |
| } |
| async function bootstrapRunRecovery(){return restoreActiveRun()} |
| async function restoreActiveRun(){try{const resumable=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(resumable?.run?.run_id){state.runId=resumable.run.run_id;if(resumable.bucket_source)state.bucketSource=resumable.bucket_source;setBuildModeInspecting(state.runId);if(resumable.view)renderRunViewModel(resumable.view,{progress:0,events:[]});setQuickLinks(resumable.view?.links||{},resumable.view||{});startPolling(state.runId);return true}}catch(e){console.warn('resumable lookup failed',e)}try{const raw=localStorage.getItem('asf.activeRun');if(!raw)return false;const saved=JSON.parse(raw);if(saved?.bucketName)state.bucketName=saved.bucketName;if(saved?.runId){state.runId=saved.runId;setText('detailRunId',saved.runId);setQuickLinks(saved.links||{},{});setBuildModeInspecting(saved.runId);startPolling(saved.runId);return true}}catch(_){}return false} |
| async function loadAppInfo(){setOperation('Checking sign-in and run storage…','loading');setBucketFeedback('checking','Checking private bucket access…');try{const me=await apiGet('/api/me');state.user=me;setText('userChip','@'+me.username);const userChip=$('userChip');if(userChip)userChip.href=`https://huggingface.co/${me.username}`;const logout=$('logoutLink');if(logout){logout.hidden=false;logout.href=me.logout_url||'/logout'}state.bucketSource=me.username+'/'+state.bucketName;setText('bucketInlineLabel',state.bucketSource);renderAuthWarnings(me);renderBillingPanel(me);loadBillingStatus().catch(()=>{});await checkBucket(false);await loadRuns(true);clearSavedRun();if(!state.bucketReady)setOperation('Create your private run bucket before launching builds.','warning');else setOperation('')}catch(e){state.user=null;setText('userChip','Sign in');const userChip=$('userChip');if(userChip)userChip.href='/oauth/huggingface/login';const logout=$('logoutLink');if(logout)logout.hidden=true;renderSignedOut();renderBillingPanel(null);setBucketReady(false,'Sign in with Hugging Face first.');showMessage('Please sign in with Hugging Face, then refresh this page.',true)}} |
| function renderSignedOut(){const auth=$('authPanel');if(!auth)return;auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML='<strong>Sign in required</strong><p>Use Hugging Face OAuth before launching Jobs or reading your Bucket.</p><a class="link-btn" href="/oauth/huggingface/login">Sign in with Hugging Face ↗</a>'} |
| function renderAuthWarnings(me){const auth=$('authPanel');if(!auth)return;const missing=me.missing_scopes||[];const warnings=me.warnings||[];if(!missing.length&&!warnings.length){auth.hidden=true;auth.innerHTML='';return}auth.hidden=false;auth.className='auth-panel warning';auth.innerHTML=`<strong>OAuth session warning</strong><p>${escapeHtml([...warnings,missing.length?'Missing scopes: '+missing.join(', '):''].filter(Boolean).join(' • '))}</p><a class="link-btn" href="/oauth/huggingface/login">Refresh sign-in ↗</a>`} |
| function renderBillingPanel(info){ |
| |
| const badge=$('billingStatusBadge'); |
| const canPay=info?.can_pay===true||info?.payment_ready===true; |
| if(badge){ |
| badge.textContent=canPay?'Billing ready':'Check billing'; |
| badge.className='badge '+(canPay?'success':'warn'); |
| } |
| const billing=$('openBilling'); |
| if(billing)billing.href=info?.links?.billing||'https://huggingface.co/settings/billing'; |
| } |
| async function loadBillingStatus(){ |
| try{const data=await apiGet('/api/billing/status');renderBillingPanel({...state.user,...data});return data}catch(e){console.warn('billing status failed',e);return null} |
| } |
| async function checkBucket(noisy=true){setButtonBusy('checkBucket',true,'Checking…');setBucketFeedback('checking','Checking private bucket access…');try{const data=await apiGet(`/api/bucket/status?bucket_name=${encodeURIComponent(state.bucketName)}`);const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Create your private run bucket before launching builds.');if(noisy)showMessage(data.ok?`Bucket ready: ${source}`:`Bucket not ready: ${data.error||'not found'}`,!data.ok);return data}catch(e){setBucketReady(false,e.message);if(noisy)showMessage(e.message,true);throw e}finally{setButtonBusy('checkBucket',false)}} |
| async function createBucket(){setButtonBusy('createBucket',true,'Creating…');setBucketFeedback('creating','Creating private bucket…');try{const data=await apiPost('/api/bucket/create',{bucket_name:state.bucketName});const source=data.bucket_source||state.bucketName;state.bucketSource=source;setText('bucketInlineLabel',source);setBucketReady(Boolean(data.ok),data.error||'Could not create bucket.');showMessage(data.ok?`Created/ready: ${source}`:`Could not create bucket: ${data.error}`,!data.ok);if(data.ok)loadRuns(true)}catch(e){setBucketReady(false,e.message);showMessage(e.message,true)}finally{setButtonBusy('createBucket',false)}} |
|
|
| function switchCenterTab(tab='active'){ |
| const next=tab==='validate'?'validate':'active'; |
| state.centerTab=next; |
| document.querySelectorAll('[data-center-tab]').forEach(btn=>{ |
| const active=btn.dataset.centerTab===next; |
| btn.classList.toggle('active',active); |
| btn.setAttribute('aria-selected',active?'true':'false'); |
| }); |
| document.querySelectorAll('[data-center-panel]').forEach(panel=>{ |
| const active=panel.dataset.centerPanel===next; |
| panel.classList.toggle('active',active); |
| panel.hidden=!active; |
| }); |
| } |
| function renderManualAction(p={}){const panel=$('manualActionPanel');if(!panel)return;const view=p.view||{};const sm=view.status_model||{};const status=String(p.status||p.state?.status||p.inference_gate?.status||sm.global_status||'');const manual=Boolean(sm.requires_manual_action||p.manual_hardware_required||p.inference_gate?.manual_hardware_required||p.hardware_strategy?.manual_action_required||status.includes('manual_hardware_required')||status.includes('generated_needs_manual_hardware')||status.includes('waiting_manual_action'));panel.hidden=!manual;if(!manual)return;setText('manualActionTitle','Manual hardware required');setText('manualActionText','Automatic hardware selection was not available. Open Space settings, choose the recommended GPU, then run Space Test.');setLink('manualOpenSettings',(p.links||view.links||state.activeLinks||{}).target_space_settings_url)} |
| function defaultValidationArgs(expected='text'){ |
| const kind=String(expected||'text').toLowerCase(); |
| if(kind==='image')return ["a cinematic robot cat astronaut, detailed, studio lighting"]; |
| if(kind==='video')return ["a short cinematic shot of a robot cat astronaut walking on the moon"]; |
| if(kind==='audio')return ["A calm voice saying hello from Agentic Space Factory."]; |
| return ["Explain the concept of agentic AI in one paragraph."]; |
| } |
| function normalizeApiName(value){ |
| value=String(value||'').trim(); |
| if(!value)return'/generate'; |
| return value.startsWith('/')?value:'/'+value; |
| } |
| function firstPositiveNumber(...values){ |
| for(const value of values.flat()){ |
| const n=Number(value); |
| if(Number.isFinite(n)&&n>0)return n; |
| } |
| return null; |
| } |
| function formatLatencySeconds(value){ |
| const n=Number(value); |
| if(!Number.isFinite(n)||n<=0)return'—'; |
| if(n<10)return`${n.toFixed(2).replace(/\.00$/,'')}s`; |
| if(n<60)return`${n.toFixed(1).replace(/\.0$/,'')}s`; |
| return fmtSeconds(Math.round(n)); |
| } |
| function parsedJsonOrNull(value){ |
| if(value==null||value==='')return null; |
| try{return typeof value==='string'?JSON.parse(value):value}catch(_){return null} |
| } |
| function validationPrefillFromDetail(detail={}){ |
| const progress=(state.activeRunProgress&&((state.activeRunProgress.run_id||state.activeRunProgress.summary?.run_id)===(detail.run_id||detail.summary?.run_id||state.runId)))?state.activeRunProgress:{}; |
| const summary={...(detail.summary||{}),...(progress.summary||{})}; |
| const smoke={...(detail.state?.generation_smoke||{}),...(detail.tests?.generation_smoke||{}),...(detail.generation_smoke||{}),...(progress.generation_smoke||{}),...(progress.state?.generation_smoke||{})}; |
| const launch={...(detail.launch||{}),...(progress.launch||{})}; |
| const stateObj={...(detail.state||{}),...(progress.state||{})}; |
| const gate={...(detail.inference_gate||{}),...(progress.inference_gate||{})}; |
| const expected=smoke.expected_output_type||gate.expected_output_type||launch.expected_output_type||summary.expected_output_type||stateObj.expected_output_type||formValue('buildExpectedOutput')||'text'; |
| let args=Array.isArray(smoke.test_args)?smoke.test_args:parsedJsonOrNull(launch.test_args_json); |
| let kwargs=smoke.test_kwargs&&typeof smoke.test_kwargs==='object'&&!Array.isArray(smoke.test_kwargs)?smoke.test_kwargs:parsedJsonOrNull(launch.test_kwargs_json); |
| if(!Array.isArray(args))args=defaultValidationArgs(expected); |
| if(!kwargs||Array.isArray(kwargs)||typeof kwargs!=='object')kwargs={}; |
| const target=summary.target_space||stateObj.target_space||launch.target_space||progress.target_space||$('detailSpace')?.textContent||''; |
| const api=normalizeApiName(smoke.api_name||gate.api_name||launch.api_name||progress.api_name||'/generate'); |
| return {target,api,expected,args,kwargs}; |
| } |
| function renderValidationStatus(data={}){ |
| state.validationDetail=data||state.validationDetail; |
| const summary=data.summary||{}; |
| const smoke=data.generation_smoke||data.summary?.generation_smoke||data.state?.generation_smoke||{}; |
| const status=data.status||data.view?.header?.status||data.state?.status||summary.status||smoke.status||'running'; |
| const normalized=String(status||'running').toLowerCase(); |
| const klass=statusClass(status); |
| const isRunning=!TERMINAL_STATUSES.has(normalized)&&klass!=='success'&&klass!=='error'; |
| const badge=$('spaceTestStatus'); |
| if(badge){badge.textContent=badgeLabel(status);badge.className='badge '+klass} |
| const tabDot=$('spaceTestTabDot'); |
| if(tabDot)tabDot.className='tab-status-dot '+(isRunning?'running':klass); |
| const card=$('spaceTestPreview'); |
| if(card)card.classList.toggle('is-validating',isRunning); |
| const runId=data.run_id||summary.run_id||state.validationRunId||'—'; |
| setText('spaceTestOutput',runId); |
| setText('lastPolled',formatTime(data.updated_at||summary.updated_at||data.state?.updated_at||data.created_at||summary.created_at||new Date().toISOString())); |
| const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,data.latency_seconds,data.observed_latency_seconds); |
| setText('spaceTestLatency',latency?formatLatencySeconds(latency):'—'); |
| setText('spaceTestVerdict',badgeLabel(status)); |
| const deleteBtn=$('deleteValidationRun'); |
| if(deleteBtn){deleteBtn.disabled=!(runId&&runId!=='—');deleteBtn.onclick=()=>deleteRunById(runId,{kind:'validation',detail:data});} |
| const percent=validationProgressPercent(data); |
| setText('spaceTestProgressPercent',`${Math.round(percent)}%`); |
| const fill=$('spaceTestProgressFill'); |
| if(fill){ |
| fill.style.width=`${Math.max(0,Math.min(100,percent))}%`; |
| fill.className='validation-progress-fill '+(isRunning?'is-running':klass); |
| } |
| renderValidationTimeline(data); |
| renderValidationEndpoints(data); |
| } |
| function renderValidationLaunchResult(result={}){ |
| state.validationRunId=result.run_id; |
| renderValidationStatus({...result,status:result.status||'running'}); |
| } |
| function renderValidationTimeline(detail={}){ |
| const root=$('spaceTestTimeline'); |
| if(!root)return; |
| const items=validationTimelineItems(detail); |
| root.innerHTML=items.map((item,idx)=>{ |
| const st=statusClass(item.status||'pending'); |
| const label=item.label||String(item.step||`Step ${idx+1}`).replace(/_/g,' '); |
| return `<div class="validation-step ${st}"><span class="dot ${st}"></span><strong>${escapeHtml(label)}</strong><em>${escapeHtml(badgeLabel(item.status||'pending'))}</em></div>`; |
| }).join('')||'<div class="empty">No validation events yet.</div>'; |
| } |
| function showValidationRunDetail(detail={}){ |
| state.validationRunId=detail.run_id||detail.summary?.run_id||state.validationRunId; |
| state.validationDetail=detail; |
| stopValidationPolling(); |
| renderValidationStatus(detail); |
| applyValidationPrefill(detail,'validation run'); |
| switchCenterTab('validate'); |
| const status=String(detail.view?.header?.status||detail.status||detail.state?.status||detail.summary?.status||'').toLowerCase(); |
| if(state.validationRunId&&!TERMINAL_STATUSES.has(status))startValidationPolling(state.validationRunId); |
| } |
| function stopValidationPolling(){ |
| if(state.validationPoll){clearInterval(state.validationPoll);state.validationPoll=null} |
| } |
| async function refreshValidationProgress(){ |
| if(!state.validationRunId)return; |
| try{ |
| const p=await apiGet(`/api/runs/${encodeURIComponent(state.validationRunId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}&include_job_logs=1`); |
| state.validationPollErrors=0; |
| renderValidationStatus(p); |
| const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase(); |
| if(TERMINAL_STATUSES.has(status)){stopValidationPolling();showMessage(`Validation finished: ${badgeLabel(status)}`,statusClass(status)==='success'?'success':statusClass(status)==='error'?'error':'warning');loadRuns(false)} |
| }catch(e){ |
| state.validationPollErrors+=1; |
| console.warn('validation polling failed',e); |
| if(state.validationPollErrors<=2)showMessage(`Validation polling failed. Retrying…`,'warning'); |
| } |
| } |
| function startValidationPolling(runId){ |
| state.validationRunId=runId; |
| switchCenterTab('validate'); |
| stopValidationPolling(); |
| refreshValidationProgress(); |
| state.validationPoll=setInterval(refreshValidationProgress,3000); |
| } |
| function applyValidationPrefill(detail={},sourceLabel='selected Space'){ |
| const prefill=validationPrefillFromDetail(detail); |
| if(prefill.target&&prefill.target!=='—')setInputValue('validateSpace',prefill.target); |
| setInputValue('expectedOutput',prefill.expected); |
| setInputValue('validateApi',prefill.api); |
| setInputValue('testArgs',prettyJson(prefill.args)); |
| setInputValue('testKwargs',prettyJson(prefill.kwargs)); |
| switchCenterTab('validate'); |
| showMessage(`Prepared validation form for ${prefill.target||sourceLabel}.`,'info'); |
| } |
| function prepareValidationFromActiveRun(){ |
| const selected=((state.selectedRunDetail?.run_id===state.runId||state.selectedRunDetail?.summary?.run_id===state.runId)?state.selectedRunDetail:{}); |
| const activeDetail={...selected,...(state.activeRunProgress||{}),summary:{...(selected.summary||{}),...(state.activeRunProgress?.summary||{})}}; |
| applyValidationPrefill(activeDetail,'active Space'); |
| } |
| function prepareValidationFromSelectedRun(){ |
| applyValidationPrefill(state.selectedRunDetail||{},'selected Space'); |
| } |
| async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,run_id:formValue('runId')||undefined,model_id:formValue('modelId'),target_space_name:formValue('targetSpace')||undefined,pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,expected_output_type:formValue('buildExpectedOutput')};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}} |
| async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const args=parseJsonField('testArgs',[]);const kwargs=parseJsonField('testKwargs',{});const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:normalizeApiName(formValue('validateApi')||'/generate'),expected_output_type:formValue('expectedOutput')||'any',test_args_json:prettyJson(args),test_kwargs_json:prettyJson(kwargs),live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);renderValidationLaunchResult(result);startValidationPolling(result.run_id);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}} |
| async function refreshRunReportPreview(runId){ |
| if(!runId)return; |
| try{ |
| const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`); |
| if(detail.bucket_source)state.bucketSource=detail.bucket_source; |
| state.selectedRunDetail=detail; |
| if(typeof renderRunDetail==='function')renderRunDetail(detail); |
| if(typeof renderActiveRunActions==='function')renderActiveRunActions(detail.summary||{},detail); |
| if(typeof renderRunDocuments==='function')renderRunDocuments(detail); |
| if(typeof renderMetrics==='function')renderMetrics({...detail,summary:detail.summary||{}}); |
| if(typeof renderBlockers==='function')renderBlockers(detail); |
| if(typeof renderRunDetail!=='function'){ |
| const reportText=(detail.report||'No report available.').slice(0,8000); |
| const report=$('reportPreview'); |
| if(report)report.textContent=reportText; |
| } |
| }catch(e){ |
| console.warn('report refresh failed',e); |
| } |
| } |
| async function refreshProgress(){if(!state.runId||state.progressPollInFlight)return;state.progressPollInFlight=true;try{const now=Date.now();const includeLogs=!state.lastJobLogPoll||now-state.lastJobLogPoll>8000;if(includeLogs)state.lastJobLogPoll=now;const requestedRunId=state.runId;const p=await apiGet(`/api/runs/${encodeURIComponent(requestedRunId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);if(requestedRunId!==state.runId)return;if((p.run_id||p.summary?.run_id)&&state.runId&&(p.run_id||p.summary?.run_id)!==state.runId)return;state.activeRunProgress=p;state.pollErrors=0;if(p.bucket_source)state.bucketSource=p.bucket_source;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);if(typeof upsertRunExplorerFromProgress==='function')upsertRunExplorerFromProgress(p);if(typeof renderActiveRunActions==='function')renderActiveRunActions({...p.summary,bucket_source:p.bucket_source},p);if(typeof renderRunDocuments==='function')renderRunDocuments({...p,summary:{...p.summary,bucket_source:p.bucket_source}});if(typeof renderAgentRecovery==='function')renderAgentRecovery(p);if(typeof renderBlockers==='function')renderBlockers(p);if(p.view){setText('detailRunId',p.view.run_id||p.run_id);setText('selectedModelId',p.view.header?.model||p.summary?.model_id||'—')}else{setText('detailRunId',p.run_id||state.runId)}renderActiveRunMeta(p);if(p.state){setText('detailSpace',p.state.target_space||p.summary?.target_space||'—');const mode=runImplementationMode(p.summary||{},p);setText('detailImplementationMode',mode?implementationModeLabel(mode):'—');const modeEl=$('detailImplementationMode');if(modeEl&&mode)modeEl.title=implementationModeDescription(mode);setText('detailHardware',p.state.selected_hardware||p.summary?.selected_hardware||'—')}setText('detailJobStatus',p.status||p.state?.status||p.view?.header?.raw_status||'running');setText('detailGateStatus',p.inference_gate?.status||p.summary?.gate_status||'—');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'—');setText('homeRunDetailStatus',badgeLabel(p.status||p.state?.status||p.view?.header?.status||'running'));setQuickLinks({...p.links,...p.view?.links,bucket_source:p.bucket_source,run_id:p.run_id},{...p,events:p.events||[]});saveActiveRun();const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;if(state.lastReportRefreshRunId!==state.runId){state.lastReportRefreshRunId=state.runId;setTimeout(()=>refreshRunReportPreview(state.runId),120);setTimeout(()=>loadRuns(true),160)}setOperation(`Run finished with status: ${badgeLabel(status)}`,status.includes('success')||status.includes('succeed')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}if(now-(state.lastRunsRefreshFromPoll||0)>15000){state.lastRunsRefreshFromPoll=now;loadRuns(false)}}catch(e){state.pollErrors+=1;console.warn(e);setOperation(`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. Retrying automatically…`,'warning')}finally{state.progressPollInFlight=false}} function numberFromEvents(events=[],keys=[]){ |
| const rows=Array.isArray(events)?events:[]; |
| for(let i=rows.length-1;i>=0;i--){ |
| const event=rows[i]||{}; |
| const data=event.data||{}; |
| for(const key of keys){ |
| const direct=Number(event[key]); |
| if(Number.isFinite(direct)&&direct>0)return direct; |
| const nested=Number(data[key]); |
| if(Number.isFinite(nested)&&nested>0)return nested; |
| } |
| } |
| return null; |
| } |
| function readRunLatencyPayload(p={}){ |
| const smoke=p.generation_smoke||p.tests?.generation_smoke||p.summary?.generation_smoke||p.state?.generation_smoke||{}; |
| const gate=p.inference_gate||p.summary?.inference_gate||{}; |
| const summary=p.summary||{}; |
| const eventLatency=numberFromEvents(p.events||[],['latency_seconds','observed_latency_seconds']); |
| const eventDuration=numberFromEvents(p.events||[],['recommended_zerogpu_duration_seconds','recommended_zero_gpu_duration_seconds','recommended_duration_seconds','zero_gpu_duration_recommendation']); |
| const latency=firstPositiveNumber(smoke.latency_seconds,smoke.observed_latency_seconds,summary.latency_seconds,summary.observed_latency_seconds,p.latency_seconds,p.observed_latency_seconds,eventLatency); |
| const duration=firstPositiveNumber(smoke.recommended_zerogpu_duration_seconds,smoke.recommended_zero_gpu_duration_seconds,smoke.recommended_duration_seconds,summary.recommended_zerogpu_duration_seconds,summary.recommended_zero_gpu_duration_seconds,summary.recommended_duration_seconds,gate.recommended_zerogpu_duration_seconds,gate.recommended_zero_gpu_duration_seconds,gate.zero_gpu_duration_recommendation,eventDuration); |
| const source=smoke.recommendation_source||summary.recommendation_source||gate.recommendation_source||(eventLatency?'generation_smoke event':''); |
| return {smoke,gate,latency,duration,source}; |
| } |
| function renderActiveLatency(p={}){ |
| const card=$('activeLatencyCard'); |
| if(!card)return; |
| const {latency,duration,source}=readRunLatencyPayload(p); |
| const hasLatency=Number.isFinite(latency)&&latency>0; |
| const hasDuration=Number.isFinite(duration)&&duration>0; |
| setText('activeLatencyValue',hasLatency?formatLatencySeconds(latency):'—'); |
| setText('activeZeroGpuDuration',hasDuration?fmtSeconds(Math.round(duration)):'—'); |
| setText('activeLatencySource',hasLatency?(source||'live smoke test'):'measured when smoke test passes'); |
| const note=$('activeLatencyNote'); |
| if(note){ |
| note.textContent=hasDuration |
| ? 'This measured latency is normally used to tune the `@spaces.GPU(duration=...)` decorator for ZeroGPU when the generated Space uses ZeroGPU.' |
| : 'When available, the live smoke-test latency is used to recommend the `@spaces.GPU(duration=...)` value for ZeroGPU apps.'; |
| } |
| card.classList.toggle('has-latency',hasLatency); |
| } |
| function runHardwareMeta(p={}){ |
| const summary=p.summary||{}; |
| const stateObj=p.state||{}; |
| const launch=p.launch||{}; |
| const hardware=p.hardware_strategy||summary.hardware_strategy||{}; |
| const target=summary.preferred_space_hardware||summary.target_gpu||stateObj.preferred_space_hardware||stateObj.target_gpu||launch.preferred_space_hardware||hardware.preferred||hardware.target||summary.selected_hardware||stateObj.selected_hardware||'—'; |
| const fallback=summary.fallback_space_hardware||summary.fallback_gpu||stateObj.fallback_space_hardware||stateObj.fallback_gpu||launch.fallback_space_hardware||hardware.fallback||'—'; |
| const output=summary.expected_output_type||stateObj.expected_output_type||launch.expected_output_type||p.generation_smoke?.expected_output_type||p.inference_gate?.expected_output_type||'—'; |
| return {target,fallback,output}; |
| } |
| function renderActiveRunMeta(p={}){ |
| const meta=runHardwareMeta(p); |
| setText('detailTargetGpu',meta.target||'—'); |
| setText('detailFallbackGpu',meta.fallback||'—'); |
| setText('detailExpectedOutput',meta.output||'—'); |
| } |
| function renderMetrics(p){const root=$('metricsList');const {smoke,gate,latency,duration}=readRunLatencyPayload(p);const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';renderActiveLatency(p);if(!root)return;root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${Number.isFinite(latency)&&latency>0?formatLatencySeconds(latency):'—'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${Number.isFinite(duration)&&duration>0?fmtSeconds(Math.round(duration)):'—'}</strong></li>`} |
| function renderEvents(events){const root=$('eventsPanel');if(!root)return;try{const rows=Array.isArray(events)?events:[];root.textContent=rows.slice(-12).map(e=>`${e?.ts||''} ${e?.step||''} ${e?.status||''} ${e?.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}catch(e){console.warn('renderEvents failed',e);root.textContent='Events could not be rendered.'}} |
| async function cancelActiveRun(){ |
| if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return} |
| if(!confirm(`Cancel Job for run ${state.runId}?`))return; |
| setButtonBusy('cancelRun',true,'Cancelling…'); |
| try{ |
| const result=await apiPost(`/api/runs/${encodeURIComponent(state.runId)}/cancel?bucket_name=${encodeURIComponent(state.bucketName)}`,{}); |
| showMessage(`Cancelled Job ${result.job_id||''}`,'success'); |
| if(state.poll){clearInterval(state.poll);state.poll=null} |
| await refreshProgress(); |
| await loadRuns(true); |
| }catch(e){ |
| showMessage(e.message,true); |
| }finally{ |
| setButtonBusy('cancelRun',false); |
| } |
| } |
| function switchView(view){state.currentView=view;document.querySelectorAll('.nav-item').forEach(b=>b.classList.toggle('active',b.dataset.view===view));document.querySelectorAll('[data-section]').forEach(s=>s.hidden=s.dataset.section!==view);setOperation('');if(view==='progress')loadRuns(false);if(view==='validate')switchCenterTab('validate');if(view==='active')switchCenterTab('active');const targetMap={build:'newBuildCard',progress:'runsExplorerPanel',validate:'spaceTestSection',active:'activeRunPanel'};const target=$(targetMap[view]||'');if(target)target.scrollIntoView({behavior:'smooth',block:'start'})} |
| function bindNavigation(){ |
| bindAllOnce('.nav-item','click','navItem',e=>switchView(e.currentTarget.dataset.view||'build')); |
| bindAllOnce('[data-view-target]','click','viewTarget',e=>switchView(e.currentTarget.dataset.viewTarget||'build')); |
| bindAllOnce('[data-center-tab]','click','centerTab',e=>switchCenterTab(e.currentTarget.dataset.centerTab||'active')); |
| } |
| function bindRunFilters(){ |
| bindAllOnce('.filter[data-status]','click','runStatusFilter',e=>{ |
| state.runStatusFilter=e.currentTarget.dataset.status||'all'; |
| state.runPage=0; |
| renderRunsFromCache(); |
| }); |
| for(const id of ['runSearch','runSearchFull'])bindOnce($(id),'input',`runSearch${id}`,e=>{ |
| state.runSearch=e.target.value||''; |
| state.runPage=0; |
| for(const other of ['runSearch','runSearchFull'])if($(other)&&$(other)!==e.target)$(other).value=state.runSearch; |
| clearTimeout(state.runSearchTimer); |
| state.runSearchTimer=setTimeout(renderRunsFromCache,80); |
| }); |
| } |
| function bindActions(){ |
| $('launchBuild')?.setAttribute('data-requires-bucket','true'); |
| $('launchBuild')?.setAttribute('data-action-button','true'); |
| $('launchValidate')?.setAttribute('data-action-button','true'); |
| const actions={ |
| launchBuild:launchBuild, |
| scanModel:scanModel, |
| launchValidate:launchValidate, |
| prepareNewBuild:prepareNewBuild, |
| manualGoValidate:prepareValidationFromActiveRun, |
| activePrefillSpaceTest:prepareValidationFromActiveRun, |
| checkBucket:()=>checkBucket(true), |
| createBucket:createBucket, |
| refreshRuns:()=>loadRuns(true), |
| refreshRunsHome:()=>loadRuns(true), |
| cancelRun:cancelActiveRun, |
| deleteActiveRun:()=>deleteRunById(state.runId,{kind:'active',detail:state.selectedRunDetail||state.activeRunProgress||{}}), |
| deleteValidationRun:()=>deleteRunById(state.validationRunId,{kind:'validation',detail:state.validationDetail||{}}), |
| }; |
| Object.entries(actions).forEach(([id,handler])=>bindOnce($(id),'click',`action${id}`,handler)); |
| bindOnce($('modelId'),'input','modelPreScanReset',resetModelPreScan); |
| bindOnce($('modelRiskAck'),'change','modelRiskAck',setBuildLaunchEnabled); |
| ['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>bindOnce($(id),'change',`hardware${id}`,()=>setText('hardwarePlan',hardwarePlanLabel()))); |
| bindOnce($('implementationMode'),'change','implementationModeHelp',updateImplementationModeHelp); |
| } |
| bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());updateImplementationModeHelp();setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo(); |
|
|
| |
| |
|
|
| |
|
|
| |
|
|