diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 4ca11d3..76f7e5d 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -19,9 +19,10 @@ func writeJSON(w http.ResponseWriter, status int, v any) { // handleHealthz reports liveness and model readiness (public). func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ - "status": "ok", - "models": s.manager != nil && s.manager.Ready(), - "version": s.modelVersion(), + "status": "ok", + "models": s.manager != nil && s.manager.Ready(), + "version": s.modelVersion(), + "max_upload_bytes": s.cfg.MaxUploadBytes, }) } diff --git a/internal/server/ui/index.html b/internal/server/ui/index.html index c459dbe..3287396 100644 --- a/internal/server/ui/index.html +++ b/internal/server/ui/index.html @@ -165,6 +165,18 @@ .link{color:var(--accent);cursor:pointer;text-decoration:none} .link:hover{text-decoration:underline} .dropzone .ext{margin-top:10px;font-family:var(--mono);font-size:11px;color:var(--faint)} +.upload-list{display:flex;flex-direction:column;gap:10px;padding:16px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px} +.upload-item{display:flex;flex-direction:column;gap:6px} +.upload-head{display:flex;justify-content:space-between;align-items:center;gap:8px} +.upload-name{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.upload-pct{font-size:12px;font-family:var(--mono);color:var(--accent);flex:none} +.upload-bar{height:6px;background:var(--border);border-radius:3px;overflow:hidden} +.upload-bar>div{height:100%;width:0%;background:var(--accent);border-radius:3px;transition:width .15s ease} +.upload-status{font-size:11px;color:var(--muted)} +.upload-item.done .upload-status{color:var(--accent)} +.upload-item.done .upload-bar>div{background:var(--ok,#22c55e)} +.upload-item.err .upload-status{color:var(--err)} +.upload-item.err .upload-bar>div{background:var(--err)} .detail-body{padding:18px 24px 60px;max-width:920px} .detail-title{display:flex;align-items:center;gap:10px;flex-wrap:wrap} @@ -356,7 +368,7 @@ const fmtTime = iso=>{ if(!iso) return "—"; const d=new Date(iso); return isNa const fmtDur = s=> s==null?"—": (s<60?s.toFixed(1)+"s" : Math.floor(s/60)+"m"+String(Math.round(s%60)).padStart(2,"0")+"s"); const escapeHtml = s=>(s||"").replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c])); -const state = { pw: localStorage.getItem("asr_pw")||"", tasks:[], sel:null, busy:false, authing:false }; +const state = { pw: localStorage.getItem("asr_pw")||"", tasks:[], sel:null, busy:false, authing:false, uploading:false, maxUpload:0 }; // --- password modal (shown only when verification is needed) --- const authModal=$("#authModal"), authPw=$("#authPw"), authMsg=$("#authMsg"), authOk=$("#authOk"), authCancel=$("#authCancel"); @@ -432,6 +444,9 @@ class ApiError extends Error{constructor(m){super(m);this.name="ApiError"}} function toast(msg,type){ const t=el("div",{class:"toast "+(type||"")}); t.textContent=msg; $("#toast").appendChild(t); setTimeout(()=>t.remove(),3200); } +// fmtSize formats a byte count as a human-readable string. +function fmtSize(b){ if(b<1024) return b+"B"; if(b<1048576) return (b/1024).toFixed(1)+"KB"; if(b<1073741824) return (b/1048576).toFixed(1)+"MB"; return (b/1073741824).toFixed(2)+"GB"; } + // Copy to clipboard with a fallback for non-secure contexts (http via IP, // where navigator.clipboard is unavailable). "done" flashes the button label. function copyText(text,btn){ @@ -464,6 +479,7 @@ function mkCopyBtn(label, getText){ async function loadHealth(){ try{ const h = await api("/healthz"); + state.maxUpload = h.max_upload_bytes || 0; $("#htext").textContent = h.models?"模型就绪":"模型加载中"; $("#hdot").className = "dot "+(h.models?"ok":"down"); }catch(e){ $("#htext").textContent="服务不可达"; $("#hdot").className="dot down"; } @@ -599,16 +615,41 @@ function renderDropzone(c){ async function uploadFiles(files){ if(!state.pw){ askForPassword("请先输入访问密码后再上传。"); return; } if(!files||!files.length) return; + state.uploading=true; + const detail=$("#detail"); + let list=$("#uploadList"); + if(!list) list=el("div",{class:"upload-list",id:"uploadList"}); + detail.prepend(list); for(const f of files){ + if(state.maxUpload && f.size>state.maxUpload){ toast("文件过大: "+f.name+"("+fmtSize(f.size)+"),上限 "+fmtSize(state.maxUpload),"err"); continue; } + const item=el("div",{class:"upload-item"}); + item.innerHTML=`