import {buildPayload, previewPayload, pythonExample, requestDecision} from './api.mjs'; import {words, examples, exampleMedia, ratingOptions} from './locales.mjs'; import {readMedia} from './media.mjs'; const $ = id => document.getElementById(id); let language = new URL(location.href).searchParams.get('lang') === 'zh' ? 'zh' : 'en'; let imageData, attachment, mediaController, mediaRequired = false, controller, resultData, requestPayload, lastError, state = 'ready', statusKey = ''; const t = key => words[language][key] || key; function renderLanguage() { document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en'; document.querySelectorAll('[data-i18n]').forEach(el => {el.textContent = t(el.dataset.i18n);}); document.querySelectorAll('[data-placeholder]').forEach(el => {el.placeholder = t(el.dataset.placeholder);}); $('language').textContent = language === 'en' ? '中文' : 'EN'; $('language').ariaLabel = language === 'en' ? '切换到中文' : 'Switch to English'; $('theme').ariaLabel = language === 'en' ? 'Switch color theme' : '切换主题'; configureOptions(); renderState(); if (resultData) renderAnswer(resultData.answer, resultData.data, requestPayload); } function configureOptions() { $('options-field').hidden = $('mode').value === 'predicate'; $('options-label').textContent = t($('mode').value === 'score' ? 'levelsLabel' : 'choicesLabel'); } function renderState() { $('state-badge').dataset.state = state; $('state-badge').textContent = t(state); $('status').className = state === 'error' ? 'error' : ''; $('status').textContent = lastError ? (words[language].errors[lastError] || `HTTP ${lastError.replace('http_', '')}`) : statusKey ? t(statusKey) : ''; } function clearResult() { resultData = null; lastError = null; state = 'ready'; statusKey = ''; $('answer').hidden = true; $('empty').hidden = false; $('response-json').textContent = ''; $('probabilities').replaceChildren();renderState(); } function showError(error) { state = 'error'; lastError = error instanceof TypeError ? 'network' : error.message; if (!words[language].errors[lastError] && !/^http_\d+$/.test(lastError)) lastError = 'response'; renderState(); } function currentPayload() { if(mediaRequired&&!imageData)throw new Error('image'); return buildPayload({model:$('model').value,mode:$('mode').value,question:$('question').value, context:$('context').value,options:$('options').value,image:imageData}); } function updatePreview(report = false) { try { const payload = currentPayload(); $('python-code').textContent = pythonExample(payload,attachment); $('request-json').textContent = JSON.stringify(previewPayload(payload),null,2); $('copy-code').disabled = false;return payload; } catch (error) { $('python-code').textContent = t('noCode');$('request-json').textContent = '';$('copy-code').disabled = true; if (report) showError(error); } } function renderAnswer(answer, data, payload) { $('empty').hidden = true;$('answer').hidden = false; const isPredicate = payload.questions[0].type === 'predicate'; $('decision-title').textContent = isPredicate ? t(answer.title) : answer.title; $('probabilities').replaceChildren(); for (const row of answer.rows) { const label = isPredicate ? t(row.label) : row.label; const block = document.createElement('div');block.className = 'prob-row'; const top = document.createElement('div');top.className = 'prob-label'; const name = document.createElement('span');name.textContent = label; const number = document.createElement('strong');number.textContent = `${(row.probability*100).toFixed(1)}%`;top.append(name,number); const progress = document.createElement('progress');progress.max=1;progress.value=row.probability;progress.ariaLabel=label; block.append(top,progress);$('probabilities').append(block); } $('score-note').hidden = payload.questions[0].type !== 'score'; $('confidence').textContent = answer.confidence === undefined ? '' : `${t('confidence')}: ${(answer.confidence*100).toFixed(1)}%`; $('model-id').textContent = data.model_id ? `${t('modelId')}: ${data.model_id}` : ''; $('response-json').textContent = JSON.stringify(data,null,2); } function lock(value) { $('fields').disabled=value; document.querySelectorAll('[data-example]').forEach(button=>{button.disabled=value;}); $('preview-code').disabled=value; } function removeImage() { mediaController?.abort();mediaController=null; $('video-preview').pause();$('video-preview').removeAttribute('src');$('video-preview').load();$('video-preview').hidden=true; if(attachment?.objectUrl)URL.revokeObjectURL(attachment.objectUrl); attachment=undefined;imageData=undefined;mediaRequired=false;$('image').value=''; $('thumbnail').removeAttribute('src');$('image-preview').hidden=true;$('video-note').hidden=true;clearResult();updatePreview(); } async function loadMedia(fileOrExample) { removeImage();mediaRequired=true; const active=new AbortController();mediaController=active; lock(true);state='busy';statusKey='reading';renderState(); try { let file=fileOrExample; if(!(file instanceof File)) { const response=await fetch(file.src,{signal:active.signal});if(!response.ok)throw new Error('image'); file=new File([await response.blob()],file.name,{type:file.kind==='video'?'video/mp4':'image/jpeg'}); } const next=await readMedia(file,active.signal); if(active.signal.aborted){URL.revokeObjectURL(next.objectUrl);return;} attachment=next;imageData=next.image; $('thumbnail').src=imageData;$('image-name').textContent=next.name;$('image-preview').hidden=false; if(next.kind==='video'){$('video-preview').src=next.video;$('video-preview').poster=imageData;$('video-preview').hidden=false;$('video-note').hidden=false;} state='ready';statusKey='';renderState();updatePreview(); } catch(error) { if(active.signal.aborted)return; $('image').value='';showError(new Error(error.message==='size'?'size':'image'));updatePreview(); } finally {if(mediaController===active){mediaController=null;lock(false);}} } $('image').addEventListener('change',()=>{ const file=$('image').files[0];if(!file){removeImage();return;}void loadMedia(file); }); $('decision-form').addEventListener('submit', async event => { event.preventDefault();if(controller||mediaController)return; clearResult();const payload=updatePreview(true);if(!payload)return; requestPayload=payload;controller=new AbortController();const active=controller; const timeout=setTimeout(()=>active.abort('timeout'),120000); lock(true);$('cancel').hidden=false;state='busy';statusKey='waiting';renderState(); try { const result=await requestDecision(payload,active.signal);if(active.signal.aborted)return; resultData=result;state='success';statusKey='';renderState();renderAnswer(result.answer,result.data,payload); } catch(error) { if(active.signal.aborted) { if(active.signal.reason==='timeout')showError(new Error('timeout')); else{state='ready';statusKey='cancelled';renderState();} } else showError(error); } finally {clearTimeout(timeout);if(controller===active){controller=null;lock(false);$('cancel').hidden=true;}} }); $('cancel').addEventListener('click',()=>controller?.abort('cancelled')); $('remove-image').addEventListener('click',removeImage); $('language').addEventListener('click',()=>{language=language==='en'?'zh':'en';renderLanguage();updatePreview();}); $('theme').addEventListener('click',()=>{document.body.dataset.theme=document.body.dataset.theme==='dark'?'light':'dark';}); $('mode').addEventListener('change',()=>{ if($('mode').value==='score'&&!$('options').value.trim())$('options').value=ratingOptions[language];configureOptions(); }); for (const name of ['question','context','options','model','mode']) { $(name).addEventListener('input',()=>{clearResult();updatePreview();}); $(name).addEventListener('change',()=>{configureOptions();updatePreview();}); } document.querySelectorAll('[data-example]').forEach(button=>button.addEventListener('click',async()=>{ removeImage();const index=Number(button.dataset.example);const example=examples[language][index]; ['mode','question','context','options'].forEach((id,index)=>{$(id).value=example[index];}); configureOptions();if(exampleMedia[index])await loadMedia(exampleMedia[index]);else updatePreview(); $('question').focus({preventScroll:true}); $('decision-form').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'center'}); })); $('preview-code').addEventListener('click',()=>{updatePreview(true);}); $('copy-code').addEventListener('click',async()=>{ try{await navigator.clipboard.writeText($('python-code').textContent);$('copy-status').textContent=t('copied');} catch{$('copy-status').textContent=t('copyFailed');} }); document.body.dataset.theme=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'; renderLanguage();updatePreview();