diff --git "a/assets/main-BBNVSS5C.js" "b/assets/main-BBNVSS5C.js"
new file mode 100644--- /dev/null
+++ "b/assets/main-BBNVSS5C.js"
@@ -0,0 +1,236 @@
+import"./modulepreload-polyfill-B5Qt9EMX.js";import{_ as Tt,i as Bs,l as $s,e as Is,c as Ds,f as Ls,p as Os,g as Fs,d as _s,r as Gs,t as Or,h as Fr,D as nn,a as _r,j as zs,S as Fn,A as Us,K as xo,k as js,b as Gr,m as Hs,n as Ws,o as Ks}from"./preload-helper-BSrbm-kb.js";import{m as Zn,D as er,P as zr,d as Vs,b as qs,M as Js,p as Qs,a as Ys}from"./models-QsPq0tJ5.js";const Xs="/assets/hero-DgsFBh_e.webp",we=e=>`${e} `,ue={paste:we(' '),trash:we(' '),copy:we(' '),download:we(' '),cancel:we(' '),retry:we(' '),cpu:we(' '),translate:we(' '),upload:we(' '),board:we(' '),flag:we(' '),person:we(' '),help:we(' '),pin:we(' '),file:we(' '),book:we(' '),gear:we(' ')},Zs=`
+
+
+
+
+
+ Cần WebGPU để vận công
+
+
+
+ Khắc phục nâng cao
+
+
+
+
+ ${ue.retry} Kiểm tra lại
+
+
+
+
+
+
+
+
+
+
+ Chuẩn bị…
+
+ Lần đầu tải ~74 MB trọng số —
+ các lần sau mở nhanh nhờ bộ nhớ đệm của trình duyệt.
+
+
+
+
+
+
+ ${ue.retry} Thử lại
+ ${ue.flag} Báo lỗi
+
+
+
+
+
+
+
+
+
+
+
+
+ Đóng
+
+
+
+
+ Hướng dẫn
+ Dịch truyện Trung – Việt ngay trong trình duyệt: văn bản không rời khỏi
+ máy, tải mô hình một lần rồi có thể chạy offline.
+
+ Chọn mô hình — Mặc Hi 30 (~74 MB) gọn hơn; Hachimi 60
+ (~113 MB) là lựa chọn lớn hơn để so sánh bản dịch.
+ Nhập truyện — dán văn bản hoặc mở/thả file .txt
+ (UTF-8 / GB18030/GBK / Big5, tối đa 20 MB).
+ Dịch — bấm Dịch hoặc Ctrl Enter, rồi chép hay tải kết quả.
+
+ Quét tên và từ điển dùng khi nào?
+ Nên dùng trước khi dịch truyện có nhiều nhân vật, địa danh,
+ tổ chức, hoặc tên phiên âm trong bối cảnh phương Tây / Nhật Bản.
+ Quét tên sẽ gợi ý cách đọc; thêm những tên cần giữ cố định vào từ điển,
+ ví dụ 爱丽丝 → Alice hoặc 雪乃 → Yukino.
+
+ Từ điển tên riêng — giữ nhất quán tên, danh xưng và thuật ngữ
+ theo cách bạn chọn.
+ Bộ nhớ & cài đặt — cụm đã dịch được nhớ riêng theo từng
+ mô hình; dịch lại file cũ gần như tức thì.
+
+ Cần WebGPU: Chrome/Edge 113+ hoặc Safari 26+.
+ Quét tên tải thêm khoảng 4 MB ở lần dùng đầu.
+ Văn bản nguồn và bản dịch không rời khỏi thiết bị.
+ Benchmark/báo lỗi chỉ gửi khi bạn chủ động xác nhận; đoạn văn chỉ đính
+ kèm nếu bạn tự chọn.
+
+ Đóng
+
+
+
+
+ Không gửi nội dung văn bản — chỉ các thông số bên dưới.
+
+
+
+ Đính kèm đoạn bị lỗi (nguồn + bản dịch, tối đa 1000 ký tự mỗi bên)
+
+
+ JSON đầy đủ sẽ gửi
+
+
+
+ Gửi
+ Hủy
+
+
+
`;function ta(e){e.innerHTML=ea;const t={};for(const n of e.querySelectorAll("[data-ref]"))t[n.dataset.ref]=n;return t}const rt=new Map;function tr(e){if(!e?.id)throw new Error("panel cần id");if(typeof e.mount!="function")throw new Error(`panel ${e.id} cần mount()`);return rt.set(e.id,{prefers:"full",...e}),()=>{rt.delete(e.id)}}function na(e){return[...rt.values()].filter(t=>t.available?!!t.available(e):!0)}const ra=90;function oa(e,t,n){const r=()=>!matchMedia("(min-width: 900px)").matches;let s=0,o=0,a=!1;const i=()=>{a&&(a=!1,t.style.transform="",o>ra&&n())};e.addEventListener("pointerdown",l=>{r()&&(a=!0,s=l.clientY,o=0,e.setPointerCapture?.(l.pointerId))}),e.addEventListener("pointermove",l=>{a&&(o=Math.max(0,l.clientY-s),t.style.transform=`translateY(${o}px)`)}),e.addEventListener("pointerup",i),e.addEventListener("pointercancel",i),e.addEventListener("click",()=>{const l=o;o=0,l<=4&&n()})}function sa({tabs:e,overlay:t,rail:n=null,getState:r=()=>({})}){const s=new Map;let o=null,a=null,i=!1;const l=d=>n?.isOpen()&&d?.prefers==="side"?n:t;function c(d){let p=s.get(d.id);return p||(p=document.createElement("div"),p.className="panel-mount",s.set(d.id,p),d.mount(p)),p}function m(d){!d||!s.has(d)||(s.delete(d),rt.get(d)?.destroy?.())}function h(d,p){const E=document.createElement("button");E.type="button",E.className=o===d.id?"ghost paneltab on":"ghost paneltab",d.icon&&E.insertAdjacentHTML("beforeend",d.icon),E.append(d.title);const S=d.badge?.(p);if(S!=null&&S!==""){const w=document.createElement("span");w.className="tabbadge",w.textContent=String(S),E.append(w)}return E.addEventListener("click",()=>{o===d.id&&l(d)===t?M():k(d.id)}),E}function y(d){e.replaceChildren(...na(d).filter(p=>p.tab!==!1).map(p=>h(p,d)))}function k(d){const p=rt.get(d);if(!p)return;if(o&&o!==d){const w=o,f=a;o=null,a=null,m(w),f?.body.replaceChildren()}const E=c(p),S=l(p);S.title.textContent=p.title,S.body.replaceChildren(E),S===t?t.modal.open||t.modal.showModal():t.modal.open&&(i=!0,t.modal.close(),i=!1),o=d,a=S,n?.onChange?.(S!==t),p.update?.(r()),y(r())}function M(){const d=o,p=a;o=null,a=null,m(d),p?.body.replaceChildren(),n?.onChange?.(!1),t.modal.open?t.modal.close():(n?.body.replaceChildren(),y(r()))}function A(){if(!o)return;const d=l(rt.get(o));d!==a&&(d===t?M():k(o))}function v(){const d=r();y(d),o&&rt.get(o)?.update?.(d)}return t.modal.addEventListener("close",()=>{if(i)return;const d=o;o=null,a=null,m(d),n?.onChange?.(!1),t.body.replaceChildren(),n?.body.replaceChildren(),y(r())}),t.modal.addEventListener("click",d=>{d.target===t.modal&&M()}),t.closeBtn.addEventListener("click",M),t.grip&&oa(t.grip,t.modal,M),{open:k,close:M,refresh:v,rehost:A,openId:()=>o}}const aa="moxhi",ia=2,rn="tm",on="\0",ca=15e4;function Ao({modelId:e,weightsTag:t,dtype:n,tokRev:r}={}){const s=[e??"unknown",t??"dev",n??"f16"];return r&&s.push(r),s.join("|")}function la(e,t){return!!e&&!e.complete&&(e.done??0)>0&&e.file===t.file&&e.size===t.size&&e.encoding===t.encoding&&e.model===t.model}function ua(){return new Promise((e,t)=>{const n=indexedDB.open(aa,ia);n.onupgradeneeded=()=>{const r=n.result;r.objectStoreNames.contains(rn)||r.createObjectStore(rn).createIndex("ts","ts"),r.objectStoreNames.contains("partial")&&r.deleteObjectStore("partial")},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)})}async function at(e,t){const n=await ua();try{return await new Promise((r,s)=>{const o=n.transaction(rn,e),a=t(o.objectStore(rn));o.oncomplete=()=>r(typeof a=="function"?a():a),o.onerror=()=>s(o.error),o.onabort=()=>s(o.error??new Error("tx aborted"))})}finally{n.close()}}async function da(e,t){const n=new Array(t.length).fill(null);try{await at("readonly",r=>{t.forEach((s,o)=>{const a=r.get(e+on+s);a.onsuccess=()=>{a.result&&(n[o]=a.result.t)}})})}catch{}return n}async function ha(e,t){try{const n=Date.now();await at("readwrite",r=>{for(const[s,o]of t)r.put({t:o,ts:n},e+on+s)})}catch{}}async function fa(e=ca){try{const t=await at("readonly",r=>{const s=r.count();return()=>s.result});if(t<=e)return;let n=t-Math.floor(e*.9);await at("readwrite",r=>{const s=r.index("ts").openCursor();s.onsuccess=()=>{const o=s.result;!o||n<=0||(o.delete(),n--,o.continue())}})}catch{}}async function ma({sample:e=64}={}){try{const t=await at("readonly",n=>{const r=[],s=n.openKeyCursor();return s.onsuccess=()=>{const o=s.result;if(!o)return;const a=String(o.key),i=a.indexOf(on);if(i<0)return;const l=a.slice(0,i);r.push(l),o.continue(l+"")},()=>r});return t.length?await at("readonly",n=>{const r=t.map(s=>({ns:s,rows:0,bytes:0}));return t.forEach((s,o)=>{const a=IDBKeyRange.bound(s+on,s+"",!1,!0),i=n.count(a);i.onsuccess=()=>{r[o].rows=i.result};const l=n.getAllKeys(a,e),c=n.getAll(a,e);c.onsuccess=()=>{const m=c.result.length;if(!m)return;const h=(l.result??[]).reduce((y,k)=>y+String(k).length,0)+c.result.reduce((y,k)=>y+(k?.t?.length??0),0);r[o].bytes=Math.round(h/m*2*r[o].rows)}}),()=>r}):[]}catch{return[]}}async function pa(e){try{await at("readwrite",t=>{t.delete(IDBKeyRange.bound(e+"\0",e+"",!1,!0))})}catch{}}function ga(e){const[t="",n="",r="",s=null]=String(e??"").split("|");return{model:t,weights:n,dtype:r,tokRev:s}}function ba(e){const t=/^weights\.([0-9a-f]{6,})\./.exec(String(e??""));return t?t[1]:String(e??"")}function va(e){return e?e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} KB`:`${(e/1048576).toFixed(1)} MB`:"—"}const ya=5e3;function wa({getNamespace:e=()=>null,enabled:t=!0,onChange:n=()=>{},prefs:r=null}={}){let s=null,o=[],a=!1,i=!1,l=null,c=0;const m=()=>o.reduce((p,E)=>p+E.rows,0);async function h(){if(!a){a=!0,d();try{o=(await ma()).sort((p,E)=>E.rows-p.rows)}catch{o=[]}i=!0,d(),n(),a=!1}}function y(){clearTimeout(c),l=null}function k(p){if(l!==p){l=p,clearTimeout(c),c=setTimeout(()=>{l=null,d()},ya),d();return}y(),pa(p).then(h)}function M(p,E="hint"){const S=document.createElement("p");return S.className=E,S.textContent=p,S}function A(p,E,S,w){const f=document.createElement("li");f.className="tmrow";const{model:N,weights:T,dtype:P,tokRev:_}=ga(p.ns),g=document.createElement("div");g.className="tmrow-main";const V=document.createElement("b");if(V.textContent=N||"(không rõ)",V.title=[ba(T),P,_&&`tok ${_}`].filter(Boolean).join(" · "),g.append(V),E){const te=document.createElement("span");te.className="tmcur",te.textContent="đang dùng",g.append(te)}const $=document.createElement("span");$.className="tmrow-meta",$.textContent=`${p.rows.toLocaleString("vi-VN")} cụm`+(p.bytes?` · ≈ ${va(p.bytes)}`:"");const re=document.createElement("button");return re.type="button",re.className=w===p.ns?"ghost tmdanger":"ghost",re.textContent=w===p.ns?"Xóa thật?":"Xóa",re.addEventListener("click",()=>S(p.ns)),f.append(g,$,re),f}function v(){const p=document.createElement("label");p.className="auto";const E=document.createElement("input");E.type="checkbox",E.id="auto-toggle",E.name="auto-toggle",E.checked=r.autoTranslate(),E.addEventListener("change",()=>r.setAutoTranslate(E.checked)),p.append(E," Tự dịch khi ngừng gõ");const S=document.createElement("hr");return S.className="panel-sep",[p,M("Chỉ với đoạn ngắn, không áp dụng khi mở file."),S]}function d(){if(!s)return;const p=e(),E=r?v():[];if(E.push(M("Cụm đã dịch được nhớ trong máy — dịch lại là có ngay.")),t||E.push(M("Phiên này đang chạy với ?tm=0 — không đọc cũng không ghi bộ nhớ.")),!i)E.push(M("đang đọc…"));else if(!o.length)E.push(M("Chưa có gì trong bộ nhớ dịch — dịch một file rồi quay lại."));else{const S=document.createElement("ul");S.className="tmlist",S.append(...o.map(w=>A(w,w.ns===p,k,l))),E.push(S),E.push(M((o.length>1?`Tổng ${m().toLocaleString("vi-VN")} cụm. `:"")+"Xóa chỉ bỏ bộ nhớ — bản dịch đang mở không mất."))}s.replaceChildren(...E)}return h(),{id:"memory",tab:!1,title:r?"Bộ nhớ & cài đặt":"Bộ nhớ dịch",prefers:"full",badge:()=>i&&m()?m().toLocaleString("vi-VN"):null,mount(p){s=p,d()},update(){y(),h()},destroy(){clearTimeout(c)}}}function Sa({store:e,autoStore:t=null,autoEnabled:n=!1,onChange:r=()=>{}}={}){let s=null,o="",a=null,i=null,l=null;function c(p){const E=document.createElement("p");return E.className="hint",E.textContent=p,E}function m(){const p=document.createElement("form");p.className="glossform";const E=document.createElement("input");E.placeholder="名字 (tiếng Trung)",E.setAttribute("aria-label","Tên tiếng Trung");const S=document.createElement("input");S.placeholder="Dịch thành…",S.setAttribute("aria-label","Tên tiếng Việt");const w=document.createElement("button");return w.type="submit",w.className="ghost",w.textContent="Thêm",p.append(E,S,w),p.addEventListener("submit",f=>{f.preventDefault(),e.add({zh:E.value,vi:S.value})&&s.querySelector(".glossform input")?.focus()}),p}function h(p){const E=document.createElement("tr");p.enabled||(E.className="glossoff");const S=document.createElement("td");S.className="glosszh",S.textContent=p.zh;const w=document.createElement("td");w.textContent=p.vi;const f=document.createElement("td");f.className="num";const N=document.createElement("input");N.type="checkbox",N.checked=p.enabled,N.title=p.enabled?"Đang áp dụng — bỏ tick để tắt":"Đang tắt",N.addEventListener("change",()=>e.update(p.zh,{enabled:N.checked})),f.append(N);const T=document.createElement("td");T.className="num";const P=document.createElement("button");return P.type="button",P.className="ghost",P.textContent="Xóa",P.addEventListener("click",()=>e.remove(p.zh)),T.append(P),E.append(S,w,f,T),E}function y(p){const E=document.createElement("table");E.className="tmtable glosstable";const S=document.createElement("thead"),w=document.createElement("tr");for(const[N,T]of[["Tên gốc",""],["Dịch thành",""],["Áp","num"],["","num"]]){const P=document.createElement("th");P.className=T,P.textContent=N,w.append(P)}S.append(w);const f=document.createElement("tbody");return f.append(...p.map(h)),E.append(S,f),E}function k(p){return p.status==="suppressed"?"đã tắt":p.status==="frequency"?"tên 2 chữ cần xuất hiện ít nhất 2 lần":p.status==="overridden"?"mục riêng của bạn đang được ưu tiên":p.overflow?`${p.overflow} lượt vượt giới hạn marker`:p.protected?`đã bảo vệ ${p.protected} lượt`:`${p.count} lượt trong văn bản`}function M(p){const E=document.createElement("tr");p.status!=="eligible"&&(E.className="glossoff");const S=document.createElement("td");S.className="glosszh";const w=document.createElement("span");w.className="autobadge",w.textContent="tự động",S.append(p.zh,w);const f=document.createElement("td");if(l===p.zh){const g=document.createElement("form");g.className="glossautoedit";const V=document.createElement("input");V.value=p.vi,V.setAttribute("aria-label",`Sửa cách dịch ${p.zh}`);const $=document.createElement("button");$.type="submit",$.className="ghost",$.textContent="Lưu";const re=document.createElement("button");re.type="button",re.className="ghost",re.textContent="Hủy",re.addEventListener("click",()=>{l=null,d()}),g.addEventListener("submit",te=>{te.preventDefault();const C=V.value.trim();C&&(l=null,o=`đã chuyển ${p.zh} thành mục riêng`,e.add({zh:p.zh,vi:C}),t.suppress(p.zh,!0))}),g.append(V,$,re),f.append(g)}else{f.append(p.vi);const g=document.createElement("small");g.className="glossreason",g.textContent=k(p),f.append(g)}const N=document.createElement("td");N.className="num glossapply";const T=document.createElement("input");T.type="checkbox",T.checked=p.status!=="suppressed",T.disabled=p.status==="frequency"||p.status==="overridden",T.title=T.disabled?k(p):T.checked?"Đang tự động áp dụng — bỏ tick để tắt":"Đã tắt cho các lần dịch sau",T.addEventListener("change",()=>t.suppress(p.zh,!T.checked)),N.append(T);const P=document.createElement("td");P.className="num glossact";const _=document.createElement("button");return _.type="button",_.className="ghost",_.textContent="Sửa",_.addEventListener("click",()=>{l=p.zh,d()}),P.append(_),E.append(S,f,N,P),E}function A(p){const E=document.createElement("table");E.className="tmtable glosstable glossautotable";const S=document.createElement("thead"),w=document.createElement("tr");for(const[N,T]of[["Tên gốc",""],["Dịch thành",""],["Áp","num"],["","num"]]){const P=document.createElement("th");P.className=T,P.textContent=N,w.append(P)}S.append(w);const f=document.createElement("tbody");return f.append(...p.map(M)),E.append(S,f),E}function v(){const p=document.createElement("div");p.className="glosstools";const E=document.createElement("input");E.type="file",E.accept=".json,application/json",E.hidden=!0;const S=document.createElement("button");S.type="button",S.className="ghost",S.textContent="Nhập JSON",S.addEventListener("click",()=>E.click()),E.addEventListener("change",()=>{const f=E.files[0];if(!f)return;const N=new FileReader;N.onload=()=>{try{const{added:T,updated:P}=e.importJson(String(N.result));o=`đã nhập ${T} tên mới, cập nhật ${P}`}catch(T){o=String(T?.message??T)}d()},N.readAsText(f)});const w=document.createElement("button");return w.type="button",w.className="ghost",w.textContent="Xuất JSON",w.addEventListener("click",()=>{const f=document.createElement("a");f.href=URL.createObjectURL(new Blob([e.exportJson()],{type:"application/json"})),f.download="tu-dien.json",f.click(),URL.revokeObjectURL(f.href)}),p.append(S,w,E),p}function d(){if(!s)return;const p=e.list(),E=[c("Tên trong bảng luôn được dịch đúng như bạn chọn.")];if(E.push(m()),p.length?E.push(y(p)):E.push(c("Chưa có tên nào — thêm ở trên, hoặc nhập từ file JSON.")),E.push(v()),n&&t){const S=document.createElement("h3");S.className="glossautohead",S.textContent="Tên khó tự động",E.push(S);const w=t.listDocumentTerms();w.length?E.push(A(w)):E.push(c("Tên khó khớp trong văn bản sẽ hiện ở đây sau lần dịch kế tiếp."))}o&&E.push(c(o)),s.replaceChildren(...E)}return{id:"glossary",title:"Từ điển tên riêng",icon:ue.book,prefers:"full",badge:()=>{const p=n&&t?t.listDocumentTerms().filter(E=>E.status==="eligible").length:0;return e.count()+p||null},mount(p){s=p,a=e.subscribe(()=>{d(),r()}),i=t?.subscribe(()=>{d(),r()})??null,d()},update(){d()},destroy(){a?.(),a=null,i?.(),i=null}}}function Ea(e={}){let t={...e};const n=new Set;return{get:()=>t,set(r){const s=typeof r=="function"?r(t):{...t,...r};if(s===t)return t;t=s;for(const o of[...n])o(t);return t},subscribe(r){return n.add(r),()=>n.delete(r)}}}const ka={translate:new Set(["translate","analyze"]),analyze:new Set};function Ma({onStart:e,onIdle:t,onError:n}={}){const r=Ea({job:null});let s=null,o=null;const a=()=>r.get().job,i=c=>{const m=a();m&&r.set({job:{...m,...c}})};async function l(c){s=new AbortController,r.set({job:{kind:c.kind,label:c.label??"",phase:c.phase??"",done:0,total:c.total??0,cancellable:c.cancellable!==!1}}),e?.(a());try{return await c.exec({signal:s.signal,progress:i})}finally{s=null,r.set({job:null}),t?.();const m=o;o=null,m&&queueMicrotask(()=>{l(m).catch(h=>n?.(h))})}}return{run(c){const m=a();return m?ka[c.kind]?.has(m.kind)?(o=c,Promise.resolve("queued")):Promise.resolve("refused"):l(c).then(()=>"ran")},cancel(){s?.abort()},cancelAll(){o=null,s?.abort()},current:a,subscribe(c){return r.subscribe(()=>c(a()))}}}const Ca=/[\s:,;(\-–—…]/;function nr(e){const t=[];let n="",r=!1;const s=o=>{n&&(t.push({text:n,q:o}),n="")};for(let o=0;o