diff --git "a/assets/main-CKq2fiQB.js" "b/assets/main-CKq2fiQB.js"
new file mode 100644--- /dev/null
+++ "b/assets/main-CKq2fiQB.js"
@@ -0,0 +1,236 @@
+import"./modulepreload-polyfill-B5Qt9EMX.js";import{_ as Rt,i as Ns,l as Ps,e as Bs,c as $s,f as Is,p as Ds,g as Ls,d as Os,r as Fs,t as Or,h as Fr,D as nn,a as _r,j as _s,S as Fn,A as zs,K as Mo,k as Gs,n as Us,b as zr,m as js,o as Hs}from"./preload-helper-DpiWnaYd.js";import{m as Zn,D as er,P as Gr,d as Ws,b as Ks,M as Vs,p as qs,a as Js}from"./models-QsPq0tJ5.js";const Qs="/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(' ')},Ys=`
+
+
+
+
+
+ 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 29 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 Zs(e){e.innerHTML=Xs;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 ea(e){return[...rt.values()].filter(t=>t.available?!!t.available(e):!0)}const ta=90;function na(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>ta&&n())};e.addEventListener("pointerdown",c=>{r()&&(a=!0,s=c.clientY,o=0,e.setPointerCapture?.(c.pointerId))}),e.addEventListener("pointermove",c=>{a&&(o=Math.max(0,c.clientY-s),t.style.transform=`translateY(${o}px)`)}),e.addEventListener("pointerup",i),e.addEventListener("pointercancel",i),e.addEventListener("click",()=>{const c=o;o=0,c<=4&&n()})}function ra({tabs:e,overlay:t,rail:n=null,getState:r=()=>({})}){const s=new Map;let o=null,a=null,i=!1;const c=d=>n?.isOpen()&&d?.prefers==="side"?n:t;function l(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&&c(d)===t?x():k(d.id)}),E}function y(d){e.replaceChildren(...ea(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=l(p),S=c(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 x(){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=c(rt.get(o));d!==a&&(d===t?x():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&&x()}),t.closeBtn.addEventListener("click",x),t.grip&&na(t.grip,t.modal,x),{open:k,close:x,refresh:v,rehost:A,openId:()=>o}}const oa="moxhi",sa=2,rn="tm",on="\0",aa=15e4;function Co({modelId:e,weightsTag:t,dtype:n,tokRev:r}={}){const s=[e??"unknown",t??"dev",n??"f16"];return r&&s.push(r),s.join("|")}function ia(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 ca(){return new Promise((e,t)=>{const n=indexedDB.open(oa,sa);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 ca();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 la(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 ua(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 da(e=aa){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 ha({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 c=a.slice(0,i);r.push(c),o.continue(c+"")},()=>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 c=n.getAllKeys(a,e),l=n.getAll(a,e);l.onsuccess=()=>{const m=l.result.length;if(!m)return;const h=(c.result??[]).reduce((y,k)=>y+String(k).length,0)+l.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 fa(e){try{await at("readwrite",t=>{t.delete(IDBKeyRange.bound(e+"\0",e+"",!1,!0))})}catch{}}function ma(e){const[t="",n="",r="",s=null]=String(e??"").split("|");return{model:t,weights:n,dtype:r,tokRev:s}}function pa(e){const t=/^weights\.([0-9a-f]{6,})\./.exec(String(e??""));return t?t[1]:String(e??"")}function ga(e){return e?e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} KB`:`${(e/1048576).toFixed(1)} MB`:"—"}const ba=5e3;function va({getNamespace:e=()=>null,enabled:t=!0,onChange:n=()=>{},prefs:r=null}={}){let s=null,o=[],a=!1,i=!1,c=null,l=0;const m=()=>o.reduce((p,E)=>p+E.rows,0);async function h(){if(!a){a=!0,d();try{o=(await ha()).sort((p,E)=>E.rows-p.rows)}catch{o=[]}i=!0,d(),n(),a=!1}}function y(){clearTimeout(l),c=null}function k(p){if(c!==p){c=p,clearTimeout(l),l=setTimeout(()=>{c=null,d()},ba),d();return}y(),fa(p).then(h)}function x(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:R,dtype:P,tokRev:_}=ma(p.ns),g=document.createElement("div");g.className="tmrow-main";const V=document.createElement("b");if(V.textContent=N||"(không rõ)",V.title=[pa(R),P,_&&`tok ${_}`].filter(Boolean).join(" · "),g.append(V),E){const ee=document.createElement("span");ee.className="tmcur",ee.textContent="đang dùng",g.append(ee)}const $=document.createElement("span");$.className="tmrow-meta",$.textContent=`${p.rows.toLocaleString("vi-VN")} cụm`+(p.bytes?` · ≈ ${ga(p.bytes)}`:"");const ne=document.createElement("button");return ne.type="button",ne.className=w===p.ns?"ghost tmdanger":"ghost",ne.textContent=w===p.ns?"Xóa thật?":"Xóa",ne.addEventListener("click",()=>S(p.ns)),f.append(g,$,ne),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,x("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(x("Cụm đã dịch được nhớ trong máy — dịch lại là có ngay.")),t||E.push(x("Phiên này đang chạy với ?tm=0 — không đọc cũng không ghi bộ nhớ.")),!i)E.push(x("đang đọc…"));else if(!o.length)E.push(x("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,c))),E.push(S),E.push(x((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(l)}}}function ya({store:e,autoStore:t=null,autoEnabled:n=!1,onChange:r=()=>{}}={}){let s=null,o="",a=null,i=null,c=null;function l(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 R=document.createElement("td");R.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)),R.append(P),E.append(S,w,f,R),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,R]of[["Tên gốc",""],["Dịch thành",""],["Áp","num"],["","num"]]){const P=document.createElement("th");P.className=R,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 x(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(c===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 ne=document.createElement("button");ne.type="button",ne.className="ghost",ne.textContent="Hủy",ne.addEventListener("click",()=>{c=null,d()}),g.addEventListener("submit",ee=>{ee.preventDefault();const M=V.value.trim();M&&(c=null,o=`đã chuyển ${p.zh} thành mục riêng`,e.add({zh:p.zh,vi:M}),t.suppress(p.zh,!0))}),g.append(V,$,ne),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 R=document.createElement("input");R.type="checkbox",R.checked=p.status!=="suppressed",R.disabled=p.status==="frequency"||p.status==="overridden",R.title=R.disabled?k(p):R.checked?"Đang tự động áp dụng — bỏ tick để tắt":"Đã tắt cho các lần dịch sau",R.addEventListener("change",()=>t.suppress(p.zh,!R.checked)),N.append(R);const P=document.createElement("td");P.className="num glossact";const _=document.createElement("button");return _.type="button",_.className="ghost",_.textContent="Sửa",_.addEventListener("click",()=>{c=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,R]of[["Tên gốc",""],["Dịch thành",""],["Áp","num"],["","num"]]){const P=document.createElement("th");P.className=R,P.textContent=N,w.append(P)}S.append(w);const f=document.createElement("tbody");return f.append(...p.map(x)),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:R,updated:P}=e.importJson(String(N.result));o=`đã nhập ${R} tên mới, cập nhật ${P}`}catch(R){o=String(R?.message??R)}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=[l("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(l("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(l("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(l(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 wa(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 Sa={translate:new Set(["translate","analyze"]),analyze:new Set};function Ea({onStart:e,onIdle:t,onError:n}={}){const r=wa({job:null});let s=null,o=null;const a=()=>r.get().job,i=l=>{const m=a();m&&r.set({job:{...m,...l}})};async function c(l){s=new AbortController,r.set({job:{kind:l.kind,label:l.label??"",phase:l.phase??"",done:0,total:l.total??0,cancellable:l.cancellable!==!1}}),e?.(a());try{return await l.exec({signal:s.signal,progress:i})}finally{s=null,r.set({job:null}),t?.();const m=o;o=null,m&&queueMicrotask(()=>{c(m).catch(h=>n?.(h))})}}return{run(l){const m=a();return m?Sa[l.kind]?.has(m.kind)?(o=l,Promise.resolve("queued")):Promise.resolve("refused"):c(l).then(()=>"ran")},cancel(){s?.abort()},cancelAll(){o=null,s?.abort()},current:a,subscribe(l){return r.subscribe(()=>l(a()))}}}const ka=/[\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