diff --git a/frontend/index.html b/frontend/index.html index 9f00950..f77b52d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -171,6 +171,7 @@ const drop=document.getElementById('drop'), fileInput=document.getElementById('f logBox=document.getElementById('logBox'), logHint=document.getElementById('logHint'); let chosen=null, polling=null, currentJobId=null, sheetPage={}, viewerZoom=1, reviewDirty=false; +let defaultVisionModel='', defaultTextModel=''; function modelLabel(m){ // Include per-1M-token pricing when the catalog provides it. @@ -212,6 +213,12 @@ function saveModelsCache(data){ try{ localStorage.setItem(MODELS_CACHE_KEY, JSON.stringify({ts:Date.now(), data})); } catch(e){} } +function fetchWithTimeout(url, ms){ + return Promise.race([ + fetch(url, {cache:'no-store'}), + new Promise((_,reject)=>setTimeout(()=>reject(new Error('timeout')), ms)) + ]); +} async function loadModels(){ const note=document.getElementById('modelNote'); @@ -225,7 +232,8 @@ async function loadModels(){ return; } try{ - const res=await fetch('/models'); + note.textContent='fetching models...'; + const res=await fetchWithTimeout('/models', 5000); if(!res.ok) throw new Error('models HTTP '+res.status); const data=await res.json(); saveModelsCache(data); @@ -235,10 +243,12 @@ async function loadModels(){ modelsLoaded=true; note.textContent='('+(data.text||[]).length+' text / '+(data.vision||[]).length+' vision available)'; }catch(err){ - visionSel.innerHTML=''; - textSel.innerHTML=''; + const fallback=[{id:defaultVisionModel||'', name:defaultVisionModel||'(default)', prompt_usd_per_mtok:null, completion_usd_per_mtok:null}]; + fillSelect(visionSel, fallback.filter(m=>m.id), defaultVisionModel); + const fallbackText=[{id:defaultTextModel||'', name:defaultTextModel||'(default)', prompt_usd_per_mtok:null, completion_usd_per_mtok:null}]; + fillSelect(textSel, fallbackText.filter(m=>m.id), defaultTextModel); visionSel.disabled=false; textSel.disabled=false; - note.textContent='using configured defaults (list unavailable)'; + note.textContent='using configured defaults ('+(err.message==='timeout'?'fetch timed out':'list unavailable')+')'; console.warn('Could not load models:', err); } } @@ -674,11 +684,17 @@ async function finalizeReview(){ // If opened from an email link (/?job=), load that job's results directly. (function init(){ - // Show the deployed build in the header so it's obvious which version is up. - fetch('/health').then(r=>r.ok?r.json():null).then(h=>{ - if(h&&h.build) document.getElementById('buildTag').textContent=' · build '+h.build; - }).catch(()=>{}); - loadModels(); + // Fetch /health first: build tag for the header and default models as a + // fallback if the larger /models catalog fails or times out. + fetch('/health', {cache:'no-store'}).then(r=>r.ok?r.json():null).then(h=>{ + if(h){ + if(h.build) document.getElementById('buildTag').textContent=' · build '+h.build; + if(h.model) defaultVisionModel=h.model; + if(h.text_model) defaultTextModel=h.text_model; + } + }).catch(()=>{}).finally(()=>{ + loadModels(); + }); const jobId=new URLSearchParams(location.search).get('job'); if(jobId){ statusEl.innerHTML='Loading job '+esc(jobId)+'...'; poll(jobId); } })();