qwerty-cli 0.0.1-alpha.19 → 0.0.1-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ import{b as wt,e as Ct,h as Et}from"./chunk-6ROGUGNX.js";import{a as ht,b as gt,c as xt,d as bt,e as yt,f as Tt}from"./chunk-BIBS2Q3E.js";import{e as pt}from"./chunk-EBAA2ZKH.js";import{b as _}from"./chunk-MFGIEKBU.js";import"./chunk-VTIB5Q36.js";import{a as V,b as kt,c as Wt}from"./chunk-G3DQB7FI.js";import{a as Mt}from"./chunk-GULN5HRV.js";import{b as St,e as L}from"./chunk-QG7ZTS2G.js";import{b as Q,d as A,f as s,g as It}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as H,useEffect as Y,useRef as ct}from"react";import{Box as p,Text as x,useApp as Ce,useInput as j}from"ink";function At(t,e=Math.random){let r=[...t];for(let n=r.length-1;n>0;n--){let o=Math.floor(e()*(n+1)),u=r[n];r[n]=r[o],r[o]=u}return r}function Bt(t){let e=t>>>0;return()=>{e=e+1831565813>>>0;let r=Math.imul(e^e>>>15,1|e);return r=r+Math.imul(r^r>>>7,61|r)^r,((r^r>>>14)>>>0)/4294967296}}function Pt(t,e){if(e<=0)throw new Error("chapterSize must be positive");let r=[];for(let n=0;n<t.length;n+=e)r.push(t.slice(n,n+e));return r}function vt(t,e,r){if(e==="random"){let n=r===void 0?Math.random:Bt(r);return At(t,n)}return t}function G(t){return{target:t,typed:"",errorsThisWord:0}}function Rt(t,e){switch(e.type){case"reset":return{state:{...t,typed:""},effect:"none"};case"backspace":return t.typed.length===0?{state:t,effect:"none"}:{state:{...t,typed:t.typed.slice(0,-1)},effect:"none"};case"char":{let r=t.typed+e.ch,n=[...t.target].slice(0,[...r].length).join("");return r===n?r.length===t.target.length?{state:{...t,typed:r},effect:"correct"}:{state:{...t,typed:r},effect:"progress"}:{state:{...t,typed:"",errorsThisWord:t.errorsThisWord+1},effect:"wrong"}}}}function Z(t,e=Date.now()){return t.length===0?{startedAt:e,results:[],current:null,finishedAt:e,playlist:t}:{startedAt:e,results:[],current:{wordIndex:0,wordStartedAt:e,input:G(t[0].name)},finishedAt:null,playlist:t}}function tt(t,e,r=Date.now()){if(!t.current)return{session:t,effect:"none"};let{state:n,effect:o}=Rt(t.current.input,e);if(o==="correct"){let u={word:n.target,errors:n.errorsThisWord,durationMs:r-t.current.wordStartedAt},d=t.current.wordIndex+1,c=[...t.results,u];return d>=t.playlist.length?{session:{...t,results:c,current:null,finishedAt:r},effect:o}:{session:{...t,results:c,current:{wordIndex:d,wordStartedAt:r,input:G(t.playlist[d].name)}},effect:o}}return{session:{...t,current:{...t.current,input:n}},effect:o}}function $t(t,e=Date.now()){if(!t.current)return{session:t,effect:"none"};let r={word:t.current.input.target,errors:0,durationMs:e-t.current.wordStartedAt,skipped:!0},n=t.current.wordIndex+1,o=[...t.results,r];return n>=t.playlist.length?{session:{...t,results:o,current:null,finishedAt:e},effect:"skipped"}:{session:{...t,results:o,current:{wordIndex:n,wordStartedAt:e,input:G(t.playlist[n].name)}},effect:"skipped"}}function et(t){let e=t.results.reduce((o,u)=>o+u.errors,0),r=(t.finishedAt??Date.now())-t.startedAt,n={};for(let o of t.results)o.errors>0&&(n[o.word]=(n[o.word]??0)+o.errors);return{wordCount:t.results.length,errors:e,durationMs:r,perWordErrors:n}}import{useEffect as Dt,useReducer as se,useRef as ae,useState as ue}from"react";import{useInput as le,useApp as de}from"ink";function me(t,e){if(e.type==="start")return{session:Z(e.playlist,e.now),lastEffect:null,effectSeq:0};if(e.type==="skip"){let r=$t(t.session,e.now);return{session:r.session,lastEffect:r.effect,effectSeq:t.effectSeq+1}}if(e.type==="event"){if(e.key.backspace||e.key.delete){let o=tt(t.session,{type:"backspace"},e.now);return{session:o.session,lastEffect:o.effect,effectSeq:t.effectSeq+1}}if(e.input.length===0)return t;let r=t.session,n=t.lastEffect;for(let o of e.input){let u=tt(r,{type:"char",ch:o},e.now);if(r=u.session,n=u.effect,r.finishedAt!==null)break}return{session:r,lastEffect:n,effectSeq:t.effectSeq+1}}return t}function fe(t){let e=[...t];if(e.some(n=>n.codePointAt(0)>=128))return{kind:"ime",cleaned:""};let r=e.filter(n=>{let o=n.codePointAt(0);return o>=32&&o<=126}).join("");return{kind:r.length>0?"valid":"noise",cleaned:r}}function Nt({playlist:t,onComplete:e,onTab:r,onEscape:n,onSkip:o,onImeBlock:u,onValidInput:d,enabled:c=!0}){let[l,b]=se(me,void 0,()=>({session:Z(t,Date.now()),lastEffect:null,effectSeq:0})),I=ae(!1),[w,y]=ue(0),{exit:S}=de();return le((f,g)=>{if(g.ctrl&&f==="c"){S();return}if(g.ctrl&&f==="n"){o?.(),b({type:"skip",now:Date.now()});return}if(g.escape){n?.();return}if(g.tab){r?.();return}if(g.upArrow||g.downArrow||g.leftArrow||g.rightArrow||g.return||g.ctrl||g.meta)return;let{kind:E,cleaned:P}=fe(f);if(E==="ime"){u?.();return}E!=="noise"&&(d?.(),b({type:"event",input:P,key:g,now:Date.now()}))},{isActive:c}),Dt(()=>{l.session.finishedAt!==null&&!I.current&&(I.current=!0,e(l.session))},[l.session,e]),Dt(()=>{if(l.session.finishedAt!==null)return;let f=setInterval(()=>y(g=>g+1),1e3);return()=>clearInterval(f)},[l.session.finishedAt]),{session:l.session,lastEffect:l.lastEffect,effectSeq:l.effectSeq,tick:w}}import{useEffect as pe,useRef as he}from"react";function Lt(t){let e=he(!1);return pe(()=>{e.current||(e.current=!0,ht(!t.enabled).catch(()=>{}))},[t.enabled]),{keystroke:()=>t.enabled&&gt(),correct:()=>t.enabled&&xt(),wrong:()=>t.enabled&&bt(),pronounce:r=>{t.enabled&&t.autoplayPronunciation&&yt(r,t.accent,t.pronunciationRate,t.pronunciationSource)},prefetch:r=>{t.enabled&&Tt(r,t.accent,t.pronunciationSource)}}}import{useCallback as ge}from"react";function Ht(t){return ge(async e=>{let r={ts:new Date().toISOString(),dictId:t.dictId,chapter:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors};await Mt(r),Et({dictId:t.dictId,chapterIndex:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors});let n=Object.entries(e.perWordErrors).filter(([,u])=>u>0);if(n.length===0)return;let o=await V();for(let[u,d]of n)o=Wt(o,u,t.dictId,d);await kt(o)},[t.dictId,t.chapterIndex,t.mode])}function xe(t=process.env){return t.KITTY_WINDOW_ID?{supportsDoubleHeight:!1}:t.VTE_VERSION?{supportsDoubleHeight:!1}:t.KONSOLE_VERSION?{supportsDoubleHeight:!1}:t.WT_SESSION?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="Apple_Terminal"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="iTerm.app"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="WezTerm"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="mintty"?{supportsDoubleHeight:!0}:t.ALACRITTY_SOCKET||t.ALACRITTY_LOG?{supportsDoubleHeight:!0}:t.XTERM_VERSION?{supportsDoubleHeight:!0}:{supportsDoubleHeight:!1}}var rt=null;function be(){return rt||(rt=xe()),rt}function F(t,e,r,n=be()){return t==="standard"||!(t==="huge"||n.supportsDoubleHeight)?!1:r>=2*e+4}import{Box as ye,Text as nt,Transform as Ot,useStdout as Te}from"ink";import{jsx as ot,jsxs as it}from"react/jsx-runtime";function K({target:t,typed:e,error:r=!1,hideTarget:n=!1,align:o="center"}){let{stdout:u}=Te(),d=u?.columns??80,c=[...t],l=[...e],b=o==="center"?Math.max(0,Math.floor((d-c.length*2)/2)):0,I=" ".repeat(Math.floor(b/2)),w=()=>c.map((y,S)=>{let f=S<l.length,g=n&&!f?"_":f?l[S]:y,E=r?s.error:f?s.accent:s.muted;return ot(nt,{bold:!0,color:E,children:g},S)});return it(ye,{flexDirection:"column",width:o==="center"?"100%":void 0,paddingY:o==="center"?2:0,children:[ot(Ot,{transform:y=>`\x1B#3${y}`,children:it(nt,{children:[I,w()]})}),ot(Ot,{transform:y=>`\x1B#4${y}`,children:it(nt,{children:[I,w()]})})]})}import{jsx as _t}from"react/jsx-runtime";function jt(t){let{cfg:e}=_(),r=process.stdout?.columns??80;return F(e.wordDisplay,[...t.target].length,r)?_t(K,{...t}):_t(It,{...t})}import{Box as v,Text as m,useStdout as we}from"ink";import{Fragment as Ie,jsx as a,jsxs as B}from"react/jsx-runtime";var qt=28;function Vt(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function Se(){let{stdout:t}=we(),e=t?.columns??80;return Math.max(20,e-qt)}function k({left:t,right:e}){let r=Se();return B(v,{children:[a(v,{width:r,children:t}),a(v,{width:qt,justifyContent:"flex-end",children:e})]})}function Gt(t){let e=A(),r=[...t.target],n=[...t.typed],o=B(v,{children:[r.map((S,f)=>{let g=f<n.length,E=t.hideTarget&&!g?"_":g?n[f]:S,P=t.error?s.error:g?s.accent:s.muted;return a(m,{bold:!0,color:P,children:E},f)}),t.phonetic&&B(Ie,{children:[a(m,{children:" "}),a(m,{italic:!0,dimColor:!0,color:s.muted,children:t.phonetic})]})]}),u=t.translation.length>0?a(m,{color:s.primary,children:t.translation[0]}):a(m,{children:" "}),d=t.phonetic?a(m,{italic:!0,dimColor:!0,color:s.muted,children:t.phonetic}):a(m,{children:" "}),c=t.info,l=Number.isInteger(c.accPct)?`${c.accPct}`:c.accPct.toFixed(1),b=!t.imeBlocked&&t.audioWarning!==null,I=t.imeBlocked?a(m,{color:s.warning,children:e.practice.imeWarningShort}):b?a(m,{color:s.warning,children:e.practice.audioWarningShort}):c.visible?a(m,{color:s.muted,children:`${c.dictName} \xB7 ${c.chapterLabel}`}):a(m,{children:" "}),w=t.imeBlocked||b?a(m,{children:" "}):c.visible?a(m,{color:s.muted,children:`${c.completed}/${c.total} \xB7 ${c.wpm}wpm \xB7 ${l}%`}):a(m,{children:" "}),y=t.imeBlocked||b?a(m,{children:" "}):c.visible?a(m,{color:s.muted,children:Vt(c.elapsedMs)}):a(m,{children:" "});return t.huge?B(v,{flexDirection:"column",children:[a(K,{target:t.target,typed:t.typed,error:t.error,hideTarget:t.hideTarget,align:"left"}),a(k,{left:d,right:I}),a(k,{left:u,right:w}),a(k,{left:a(m,{children:" "}),right:y})]}):B(v,{flexDirection:"column",children:[a(k,{left:o,right:I}),a(k,{left:a(m,{children:" "}),right:w}),a(k,{left:u,right:y})]})}function Ft(){let t=A();return B(v,{flexDirection:"column",children:[a(k,{left:a(m,{color:s.warning,children:t.stealth.paused}),right:a(m,{color:s.muted,children:t.stealth.pausedHintRight})}),a(k,{left:a(m,{children:" "}),right:B(m,{color:s.muted,children:["Esc ",t.common.back]})}),a(k,{left:a(m,{children:" "}),right:a(m,{children:" "})})]})}function Kt(t){let e=A(),r=Number.isInteger(t.accPct)?`${t.accPct}`:t.accPct.toFixed(1),n=`${e.stealth.chapterDone} \xB7 ${t.wordCount}w \xB7 ${t.wpm}wpm \xB7 ${r}% \xB7 ${Vt(t.durationMs)}`;return B(v,{flexDirection:"column",children:[a(k,{left:a(m,{color:s.success,children:n}),right:a(m,{color:s.muted,children:e.stealth.nextHintRight})}),a(k,{left:a(m,{children:" "}),right:B(m,{color:s.muted,children:["Esc ",e.common.back]})}),a(k,{left:a(m,{children:" "}),right:a(m,{children:" "})})]})}import{jsx as i,jsxs as W}from"react/jsx-runtime";function $r({params:t}){let{dictId:e,chapterIndex:r,mode:n}=t,{cfg:o}=_(),u=A(),[d,c]=H("loading"),[l,b]=H(null),[I,w]=H(null);return Y(()=>{let y=!1;return c("loading"),b(null),w(null),(async()=>{try{let S=await pt(e);if(y)return;if(n==="review"){let P=await V();if(y)return;let O=S.filter(q=>P[q.name]?.count).slice(0,o.chapterSize);if(O.length===0){w(u.practice.errors.noMistakes),c("error");return}b({playlist:O,totalChapters:1}),c("typing");return}let f=Pt(S,o.chapterSize);if(f.length===0){w(u.practice.errors.dictEmpty(e)),c("error");return}let g=Math.max(0,Math.min(f.length-1,r)),E=vt(f[g],n);b({playlist:E,totalChapters:f.length}),c("typing")}catch(S){if(y)return;w(S.message),c("error")}})(),()=>{y=!0}},[e,r,n,o.chapterSize,u]),d==="loading"?i(Pe,{text:u.practice.loading,color:s.muted}):d==="error"?i(Ae,{msg:I??u.practice.errors.unknown}):l?i(Me,{params:t,loaded:l,phase:d,setPhase:c},`${e}-${r}-${n}-${t.stealth?"s":"n"}`):null}function Me({params:t,loaded:e,phase:r,setPhase:n}){let{dictId:o,chapterIndex:u,mode:d}=t,c=t.stealth===!0,{cfg:l}=_(),b=Q(),{exit:I}=Ce(),w=()=>b.stack.length>1?b.back():I(),y=Ht({dictId:o,chapterIndex:u,mode:d}),S=St(o),f=Lt({enabled:!c&&l.sounds.master,accent:l.accent,autoplayPronunciation:!c&&l.autoplayPronunciation,pronunciationRate:l.sounds.pronunciationRate,pronunciationSource:l.sounds.pronunciationSource}),g=wt(),E=l.sounds.master?g.warning:null,P=ct(!1),O=ct(0),q=ct(-1),[zt,at]=H(!1),[ut,Xt]=H(null),[lt,dt]=H(!1);Y(()=>{if(ut===null)return;let h=setTimeout(()=>at(!1),2e3);return()=>clearTimeout(h)},[ut]);let{session:C,lastEffect:R,effectSeq:z,tick:Jt}=Nt({playlist:e.playlist,enabled:r==="typing",onComplete:h=>{P.current||(P.current=!0,n("summary"),Promise.resolve(y(et(h))).catch(T=>{console.error("Failed to persist session:",T)}))},onEscape:()=>n(r==="paused"?"typing":"paused"),onTab:c?void 0:()=>{let h=C.current?e.playlist[C.current.wordIndex]:void 0;h&&f.pronounce(h.name)},onImeBlock:()=>dt(!0),onValidInput:()=>dt(!1)});Y(()=>{c||z!==O.current&&(O.current=z,R!==null&&(R==="wrong"&&l.sounds.feedback&&f.wrong(),R==="progress"&&l.sounds.keystroke&&f.keystroke(),R==="correct"&&(l.sounds.feedback&&f.correct(),l.sounds.keystroke&&f.keystroke())))},[c,z,R,f,l.sounds.feedback,l.sounds.keystroke]),Y(()=>{if(c)return;let h=C.current?.wordIndex??-1;if(h===-1||h===q.current)return;q.current=h;let T=e.playlist[h],D=e.playlist[h+1];T&&l.autoplayPronunciation&&f.pronounce(T.name),D&&f.prefetch(D.name)},[c,C.current?.wordIndex,f,l.autoplayPronunciation,e.playlist]),j((h,T)=>{if(T.tab){at(!0),Xt(Date.now());return}},{isActive:c&&r==="typing"}),j((h,T)=>{if(T.return){n("typing");return}if(T.escape){w();return}},{isActive:r==="paused"}),j((h,T)=>{T.ctrl&&h==="c"&&(Ct(!0),I())},{isActive:c&&r==="paused"}),j((h,T)=>{if(T.escape){w();return}if(T.return){let D=u+1;d==="loop"?b.replace({name:"practice",params:{dictId:o,chapterIndex:u,mode:d,stealth:t.stealth}}):d==="review"||D>=e.totalChapters?w():b.replace({name:"practice",params:{dictId:o,chapterIndex:D,mode:d,stealth:t.stealth}});return}if(h==="m"){b.replace({name:"practice",params:{dictId:o,chapterIndex:0,mode:"review",stealth:t.stealth}});return}},{isActive:r==="summary"});let $=C.results.length,Qt=C.results.reduce((h,T)=>h+T.errors,0),X=Date.now()-C.startedAt,mt=X/6e4,ft=mt>0?Math.round($/mt*10)/10:0,M=r==="summary"?et(C):null;if(c){if(r==="paused")return i(Ft,{});if(r==="summary"&&M){let N=M.durationMs/6e4,ne=N>0?Math.round(M.wordCount/N*10)/10:0,oe=Object.keys(M.perWordErrors).length,ie=M.wordCount===0?1:Math.max(0,(M.wordCount-oe)/M.wordCount),ce=Math.round(ie*1e3)/10;return i(Kt,{wordCount:M.wordCount,errors:M.errors,durationMs:M.durationMs,wpm:ne,accPct:ce})}let h=C.current?e.playlist[C.current.wordIndex]:e.playlist[e.playlist.length-1],T=C.current?.input??{target:"",typed:"",errorsThisWord:0},D=new Set(C.results.filter(N=>N.errors>0).map(N=>N.word)).size,te=$===0?1:Math.max(0,($-D)/$),ee=Math.round(te*1e3)/10,re=d==="review"?"review":`ch ${u+1}/${e.totalChapters}`;return i(Gt,{target:h?.name??"",typed:T.typed,hideTarget:d==="dictation",phonetic:Ut(h,l.accent),translation:h?.trans??[],error:R==="wrong",huge:F(l.wordDisplay,[...h?.name??""].length,process.stdout.columns??80),imeBlocked:lt,audioWarning:E,info:{visible:zt,dictName:L(S,24),chapterLabel:re,completed:$,total:e.playlist.length,wpm:ft,accPct:ee,elapsedMs:X}})}if(r==="paused")return i(Ee,{dictName:S,chapterIndex:u,totalChapters:e.totalChapters,mode:d,completed:$,total:e.playlist.length});if(r==="summary"&&M)return i(ve,{dictName:S,chapterIndex:u,totalChapters:e.totalChapters,mode:d,summary:M});let J=C.current?e.playlist[C.current.wordIndex]:e.playlist[e.playlist.length-1],Zt=C.current?.input??{target:"",typed:"",errorsThisWord:0};return i(ke,{dictName:S,chapterIndex:u,totalChapters:e.totalChapters,mode:d,accent:l.accent,completed:$,total:e.playlist.length,errors:Qt,wpm:ft,elapsedMs:X,target:J?.name??"",typed:Zt.typed,flashError:R==="wrong",hideTarget:d==="dictation",phonetic:Ut(J,l.accent),translation:J?.trans??[],imeBlocked:lt,audioWarning:E})}function Ut(t,e){return t?(e==="us"?t.usphone:t.ukphone)??null:null}function st(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function ke(t){let e=A(),r=t.total===0?0:t.completed/t.total;return W(p,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[i(We,{dictName:t.dictName,chapterIndex:t.chapterIndex,totalChapters:t.totalChapters,mode:t.mode,accent:t.accent,completed:t.completed,total:t.total,elapsedMs:t.elapsedMs}),W(p,{flexGrow:1,flexDirection:"column",alignItems:"center",justifyContent:"center",children:[i(jt,{target:t.target,typed:t.typed,error:t.flashError,hideTarget:t.hideTarget}),t.phonetic&&i(p,{marginTop:1,children:i(x,{italic:!0,dimColor:!0,color:s.muted,children:t.phonetic})}),t.translation.length>0&&i(p,{marginTop:1,flexDirection:"column",alignItems:"center",children:t.translation.slice(0,2).map((n,o)=>i(x,{color:s.primary,children:n},o))}),t.imeBlocked&&i(p,{marginTop:1,children:i(x,{color:s.warning,children:e.practice.imeWarning})}),!t.imeBlocked&&t.audioWarning&&i(p,{marginTop:1,children:i(x,{color:s.warning,children:t.audioWarning})})]}),W(p,{flexDirection:"column",children:[i(Yt,{frac:r}),i(p,{justifyContent:"center",marginTop:1,children:W(x,{color:s.muted,children:[t.completed,"/",t.total," \xB7 ",st(t.elapsedMs)," \xB7 ",t.wpm," ",e.practice.statCards.wpm," \xB7 ",t.errors," ",e.practice.statCards.errors]})}),i(p,{justifyContent:"center",marginTop:1,children:i(x,{color:s.muted,children:e.practice.footers.typing})})]})]})}function We(t){let e=A(),r=e.practice.modes[t.mode],n=e.practice.accents[t.accent],o=L(t.dictName,20),u=t.mode==="review"?`${o} \xB7 ${e.practice.reviewLabel} \xB7 ${n}`:`${o} \xB7 ${e.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${r} \xB7 ${n}`,d=`${t.completed}/${t.total} \xB7 ${st(t.elapsedMs)}`;return W(p,{children:[i(x,{color:s.muted,children:u}),i(p,{flexGrow:1}),i(x,{color:s.muted,children:d})]})}function Yt({frac:t}){let e=process.stdout.columns??80,r=Math.max(20,Math.min(72,e-16)),n=Math.round(r*Math.max(0,Math.min(1,t))),o=r-n;return W(p,{justifyContent:"center",children:[i(x,{color:s.accent,children:"\u2501".repeat(n)}),i(x,{color:s.muted,children:"\u2500".repeat(o)})]})}function Ee(t){let e=A(),r=t.total===0?0:t.completed/t.total,n=t.mode==="review"?`${L(t.dictName,20)} \xB7 ${e.practice.reviewLabel}`:`${L(t.dictName,20)} \xB7 ${e.practice.pause.chapter(t.chapterIndex+1,t.totalChapters)}`;return W(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{bold:!0,color:s.warning,children:e.practice.pause.title}),i(p,{marginTop:1,children:i(x,{color:s.muted,children:n})}),i(p,{marginTop:2,children:i(Yt,{frac:r})}),i(p,{marginTop:1,children:i(x,{color:s.muted,children:e.practice.pause.progress(t.completed,t.total)})}),i(p,{marginTop:2,children:i(x,{color:s.muted,children:e.practice.pause.hint})})]})}function Ae({msg:t}){let e=A();return W(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{color:s.error,children:t}),i(p,{marginTop:2,children:W(x,{color:s.muted,children:["Esc ",e.common.back]})}),i(Be,{})]})}function Be(){let t=Q();return j((e,r)=>{r.escape&&t.back()}),null}function Pe({text:t,color:e}){return i(p,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(x,{color:e,children:t})})}function ve(t){let{summary:e}=t,r=e.durationMs/6e4,n=r>0?Math.round(e.wordCount/r*10)/10:0,o=Object.keys(e.perWordErrors).length,u=e.wordCount===0?1:Math.max(0,(e.wordCount-o)/e.wordCount),d=Math.round(u*1e3)/10,c=A(),l=c.practice.modes[t.mode],b=L(t.dictName,20),I=t.mode==="review"?`${b} \xB7 ${c.practice.reviewLabel}`:`${b} \xB7 ${c.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${l}`,y=`Enter ${t.mode==="loop"?c.practice.summary.loopAgain:t.mode==="review"||t.chapterIndex+1>=t.totalChapters?c.practice.summary.backMenu:c.practice.summary.nextChapter} \xB7 m ${c.practice.summary.reviewMistakes} \xB7 Esc ${c.practice.summary.backMenu}`;return W(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",paddingY:1,width:"100%",height:"100%",children:[i(x,{bold:!0,color:s.success,children:c.practice.chapterComplete}),i(p,{marginTop:1,children:i(x,{color:s.muted,children:I})}),W(p,{marginTop:3,flexDirection:"row",justifyContent:"center",children:[i(U,{label:c.practice.statCards.words,value:String(e.wordCount),color:s.text}),i(U,{label:c.practice.statCards.errors,value:String(e.errors),color:e.errors>0?s.error:s.muted}),i(U,{label:c.practice.statCards.wpm,value:String(n),color:s.accent}),i(U,{label:c.practice.statCards.accuracy,value:`${d}%`,color:s.accent})]}),i(p,{marginTop:2,children:i(x,{color:s.muted,children:c.practice.statCards.elapsed(st(e.durationMs))})}),i(p,{flexGrow:1}),i(p,{marginTop:2,children:i(x,{color:s.muted,children:y})})]})}function U({label:t,value:e,color:r}){return W(p,{flexDirection:"column",alignItems:"center",marginX:3,children:[i(x,{bold:!0,color:r,children:e}),i(x,{color:s.muted,children:t})]})}export{$r as PracticeScreen};
2
+ //# sourceMappingURL=PracticeScreen-4GUFONK5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/screens/PracticeScreen.tsx","../src/util/shuffle.ts","../src/domain/chapters.ts","../src/domain/input-buffer.ts","../src/domain/session.ts","../src/ui/hooks/useWordLoop.ts","../src/ui/hooks/useAudio.ts","../src/ui/hooks/useSessionPersistence.ts","../src/util/term-caps.ts","../src/ui/components/BigWordHuge.tsx","../src/ui/components/BigWordAuto.tsx","../src/ui/screens/StealthPracticeLayout.tsx"],"sourcesContent":["import { useState, useEffect, useRef } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport type { Mode } from '../../domain/chapters.js';\nimport { chunkChapters, buildPlaylist } from '../../domain/chapters.js';\nimport { sessionSummary } from '../../domain/session.js';\nimport { loadMistakes } from '../../domain/mistakes.js';\nimport { ensureDictionary } from '../../infra/dict-downloader.js';\nimport { useWordLoop } from '../hooks/useWordLoop.js';\nimport { useAudio } from '../hooks/useAudio.js';\nimport { useSessionPersistence } from '../hooks/useSessionPersistence.js';\nimport { useNav, type PracticeParams } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { BigWordAuto } from '../components/BigWordAuto.js';\nimport { shouldUseHuge } from '../../util/term-caps.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { StealthTyping, StealthPaused, StealthSummary } from './StealthPracticeLayout.js';\nimport { setSilentExit } from '../../util/post-exit-action.js';\n\ntype Phase = 'loading' | 'typing' | 'paused' | 'summary' | 'error';\n\ntype Loaded = {\n playlist: Word[];\n totalChapters: number;\n};\n\nexport function PracticeScreen({ params }: { params: PracticeParams }) {\n const { dictId, chapterIndex, mode } = params;\n const { cfg } = useAppState();\n const t = useStrings();\n\n const [phase, setPhase] = useState<Phase>('loading');\n const [loaded, setLoaded] = useState<Loaded | null>(null);\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n setPhase('loading');\n setLoaded(null);\n setErrorMsg(null);\n (async () => {\n try {\n const words = await ensureDictionary(dictId);\n if (cancelled) return;\n if (mode === 'review') {\n const book = await loadMistakes();\n if (cancelled) return;\n const reviewWords = words.filter((w) => book[w.name]?.count).slice(0, cfg.chapterSize);\n if (reviewWords.length === 0) {\n setErrorMsg(t.practice.errors.noMistakes);\n setPhase('error');\n return;\n }\n setLoaded({ playlist: reviewWords, totalChapters: 1 });\n setPhase('typing');\n return;\n }\n const chapters = chunkChapters(words, cfg.chapterSize);\n if (chapters.length === 0) {\n setErrorMsg(t.practice.errors.dictEmpty(dictId));\n setPhase('error');\n return;\n }\n const idx = Math.max(0, Math.min(chapters.length - 1, chapterIndex));\n const playlist = buildPlaylist(chapters[idx]!, mode);\n setLoaded({ playlist, totalChapters: chapters.length });\n setPhase('typing');\n } catch (err) {\n if (cancelled) return;\n setErrorMsg((err as Error).message);\n setPhase('error');\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [dictId, chapterIndex, mode, cfg.chapterSize, t]);\n\n if (phase === 'loading') {\n return <Centered text={t.practice.loading} color={PALETTE.muted} />;\n }\n if (phase === 'error') {\n return <ErrorView msg={errorMsg ?? t.practice.errors.unknown} />;\n }\n if (!loaded) return null;\n\n return (\n <PracticeRunner\n key={`${dictId}-${chapterIndex}-${mode}-${params.stealth ? 's' : 'n'}`}\n params={params}\n loaded={loaded}\n phase={phase}\n setPhase={setPhase}\n />\n );\n}\n\nfunction PracticeRunner({\n params,\n loaded,\n phase,\n setPhase,\n}: {\n params: PracticeParams;\n loaded: Loaded;\n phase: Phase;\n setPhase: (p: Phase) => void;\n}) {\n const { dictId, chapterIndex, mode } = params;\n const stealth = params.stealth === true;\n const { cfg } = useAppState();\n const nav = useNav();\n const { exit } = useApp();\n const goBack = () => (nav.stack.length > 1 ? nav.back() : exit());\n const persist = useSessionPersistence({ dictId, chapterIndex, mode });\n const dictName = useDictName(dictId);\n\n const audio = useAudio({\n enabled: !stealth && cfg.sounds.master,\n accent: cfg.accent,\n autoplayPronunciation: !stealth && cfg.autoplayPronunciation,\n pronunciationRate: cfg.sounds.pronunciationRate,\n pronunciationSource: cfg.sounds.pronunciationSource,\n });\n const audioStatus = useAudioStatus();\n // Only surface the warning when the user opted in to sounds — if they\n // disabled sounds via config, \"no player found\" isn't a problem to flag.\n const audioWarn = cfg.sounds.master ? audioStatus.warning : null;\n\n const finishedRef = useRef(false);\n const lastEffectSeqRef = useRef<number>(0);\n const lastIndexRef = useRef<number>(-1);\n const [infoVisible, setInfoVisible] = useState(false);\n const [infoShownAt, setInfoShownAt] = useState<number | null>(null);\n const [imeBlocked, setImeBlocked] = useState(false);\n\n useEffect(() => {\n if (infoShownAt === null) return;\n const id = setTimeout(() => setInfoVisible(false), 2000);\n return () => clearTimeout(id);\n }, [infoShownAt]);\n\n const { session, lastEffect, effectSeq, tick } = useWordLoop({\n playlist: loaded.playlist,\n enabled: phase === 'typing',\n onComplete: (s) => {\n if (finishedRef.current) return;\n finishedRef.current = true;\n setPhase('summary');\n Promise.resolve(persist(sessionSummary(s))).catch((err) => {\n console.error('Failed to persist session:', err);\n });\n },\n onEscape: () => setPhase(phase === 'paused' ? 'typing' : 'paused'),\n onTab: stealth\n ? undefined\n : () => {\n const cur = session.current ? loaded.playlist[session.current.wordIndex] : undefined;\n if (cur) void audio.pronounce(cur.name);\n },\n onImeBlock: () => setImeBlocked(true),\n onValidInput: () => setImeBlocked(false),\n });\n\n useEffect(() => {\n if (stealth) return;\n if (effectSeq === lastEffectSeqRef.current) return;\n lastEffectSeqRef.current = effectSeq;\n if (lastEffect === null) return;\n if (lastEffect === 'wrong' && cfg.sounds.feedback) audio.wrong();\n if (lastEffect === 'progress' && cfg.sounds.keystroke) audio.keystroke();\n if (lastEffect === 'correct') {\n if (cfg.sounds.feedback) audio.correct();\n if (cfg.sounds.keystroke) audio.keystroke();\n }\n }, [stealth, effectSeq, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);\n\n useEffect(() => {\n if (stealth) return;\n const idx = session.current?.wordIndex ?? -1;\n if (idx === -1) return;\n if (idx === lastIndexRef.current) return;\n lastIndexRef.current = idx;\n const cur = loaded.playlist[idx];\n const next = loaded.playlist[idx + 1];\n if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);\n if (next) audio.prefetch(next.name);\n }, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);\n\n void tick;\n\n useInput(\n (_input, key) => {\n // Node's readline normalizes byte 0x09 (Ctrl+I) to {name:'tab', ctrl:false},\n // so key.ctrl && input === 'i' would never match. Tab and Ctrl+I both arrive\n // here as key.tab — bind to that. In stealth mode Tab has no other use\n // (onTab is disabled below), so this is non-conflicting.\n if (key.tab) {\n setInfoVisible(true);\n setInfoShownAt(Date.now());\n return;\n }\n },\n { isActive: stealth && phase === 'typing' },\n );\n\n useInput(\n (_input, key) => {\n if (key.return) {\n setPhase('typing');\n return;\n }\n if (key.escape) {\n goBack();\n return;\n }\n },\n { isActive: phase === 'paused' },\n );\n\n // Stealth + paused only: Ctrl+C exits silently — erase the 3 inline rows\n // (handled in practice.impl.ts after waitUntilExit) and skip the session\n // report. Normal Ctrl+C from typing keeps the 3 rows in scrollback.\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n setSilentExit(true);\n exit();\n }\n },\n { isActive: stealth && phase === 'paused' },\n );\n\n useInput(\n (input, key) => {\n if (key.escape) {\n goBack();\n return;\n }\n if (key.return) {\n const nextIdx = chapterIndex + 1;\n if (mode === 'loop') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex, mode, stealth: params.stealth },\n });\n } else if (mode === 'review' || nextIdx >= loaded.totalChapters) {\n goBack();\n } else {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth },\n });\n }\n return;\n }\n if (input === 'm') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: 0, mode: 'review', stealth: params.stealth },\n });\n return;\n }\n },\n { isActive: phase === 'summary' },\n );\n\n const completed = session.results.length;\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const elapsedMs = Date.now() - session.startedAt;\n const minutes = elapsedMs / 60000;\n const wpm = minutes > 0 ? Math.round((completed / minutes) * 10) / 10 : 0;\n\n const summary = phase === 'summary' ? sessionSummary(session) : null;\n\n if (stealth) {\n if (phase === 'paused') return <StealthPaused />;\n if (phase === 'summary' && summary) {\n const sMinutes = summary.durationMs / 60000;\n const sWpm = sMinutes > 0 ? Math.round((summary.wordCount / sMinutes) * 10) / 10 : 0;\n const sErrWords = Object.keys(summary.perWordErrors).length;\n const sAcc =\n summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - sErrWords) / summary.wordCount);\n const sAccPct = Math.round(sAcc * 1000) / 10;\n return (\n <StealthSummary\n wordCount={summary.wordCount}\n errors={summary.errors}\n durationMs={summary.durationMs}\n wpm={sWpm}\n accPct={sAccPct}\n />\n );\n }\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n const errWords = new Set(\n session.results.filter((r) => r.errors > 0).map((r) => r.word),\n ).size;\n const accFrac =\n completed === 0 ? 1 : Math.max(0, (completed - errWords) / completed);\n const accPct = Math.round(accFrac * 1000) / 10;\n const chapterLabel =\n mode === 'review'\n ? 'review'\n : `ch ${chapterIndex + 1}/${loaded.totalChapters}`;\n return (\n <StealthTyping\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n error={lastEffect === 'wrong'}\n huge={shouldUseHuge(\n cfg.wordDisplay,\n [...(currentWord?.name ?? '')].length,\n process.stdout.columns ?? 80,\n )}\n imeBlocked={imeBlocked}\n audioWarning={audioWarn}\n info={{\n visible: infoVisible,\n dictName: truncateName(dictName, 24),\n chapterLabel,\n completed,\n total: loaded.playlist.length,\n wpm,\n accPct,\n elapsedMs,\n }}\n />\n );\n }\n\n if (phase === 'paused') {\n return (\n <PausedView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n completed={completed}\n total={loaded.playlist.length}\n />\n );\n }\n\n if (phase === 'summary' && summary) {\n return (\n <SummaryView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n summary={summary}\n />\n );\n }\n\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n\n return (\n <TypingLayout\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n accent={cfg.accent}\n completed={completed}\n total={loaded.playlist.length}\n errors={errors}\n wpm={wpm}\n elapsedMs={elapsedMs}\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n flashError={lastEffect === 'wrong'}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n imeBlocked={imeBlocked}\n audioWarning={audioWarn}\n />\n );\n}\n\nfunction pickPhonetic(word: Word | undefined, accent: 'us' | 'uk'): string | null {\n if (!word) return null;\n const p = accent === 'us' ? word.usphone : word.ukphone;\n return p ?? null;\n}\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction TypingLayout(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n errors: number;\n wpm: number;\n elapsedMs: number;\n target: string;\n typed: string;\n flashError: boolean;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n imeBlocked: boolean;\n audioWarning: string | null;\n}) {\n const t = useStrings();\n const progressFrac = props.total === 0 ? 0 : props.completed / props.total;\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <StatusBar\n dictName={props.dictName}\n chapterIndex={props.chapterIndex}\n totalChapters={props.totalChapters}\n mode={props.mode}\n accent={props.accent}\n completed={props.completed}\n total={props.total}\n elapsedMs={props.elapsedMs}\n />\n\n <Box flexGrow={1} flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\">\n <BigWordAuto\n target={props.target}\n typed={props.typed}\n error={props.flashError}\n hideTarget={props.hideTarget}\n />\n\n {props.phonetic && (\n <Box marginTop={1}>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </Box>\n )}\n\n {props.translation.length > 0 && (\n <Box marginTop={1} flexDirection=\"column\" alignItems=\"center\">\n {props.translation.slice(0, 2).map((tr, i) => (\n <Text key={i} color={PALETTE.primary}>\n {tr}\n </Text>\n ))}\n </Box>\n )}\n\n {props.imeBlocked && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.practice.imeWarning}</Text>\n </Box>\n )}\n\n {!props.imeBlocked && props.audioWarning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{props.audioWarning}</Text>\n </Box>\n )}\n </Box>\n\n <Box flexDirection=\"column\">\n <ProgressBar frac={progressFrac} />\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>\n {props.completed}/{props.total} · {fmtTime(props.elapsedMs)} · {props.wpm} {t.practice.statCards.wpm} · {props.errors} {t.practice.statCards.errors}\n </Text>\n </Box>\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.footers.typing}</Text>\n </Box>\n </Box>\n </Box>\n );\n}\n\nfunction StatusBar(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n elapsedMs: number;\n}) {\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const accentName = t.practice.accents[props.accent];\n const name = truncateName(props.dictName, 20);\n const left =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel} · ${accentName}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName} · ${accentName}`;\n const right = `${props.completed}/${props.total} · ${fmtTime(props.elapsedMs)}`;\n return (\n <Box>\n <Text color={PALETTE.muted}>{left}</Text>\n <Box flexGrow={1} />\n <Text color={PALETTE.muted}>{right}</Text>\n </Box>\n );\n}\n\nfunction ProgressBar({ frac }: { frac: number }) {\n const cols = process.stdout.columns ?? 80;\n const width = Math.max(20, Math.min(72, cols - 16));\n const filled = Math.round(width * Math.max(0, Math.min(1, frac)));\n const empty = width - filled;\n return (\n <Box justifyContent=\"center\">\n <Text color={PALETTE.accent}>{'━'.repeat(filled)}</Text>\n <Text color={PALETTE.muted}>{'─'.repeat(empty)}</Text>\n </Box>\n );\n}\n\nfunction PausedView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n completed: number;\n total: number;\n}) {\n const t = useStrings();\n const frac = props.total === 0 ? 0 : props.completed / props.total;\n const subtitle =\n props.mode === 'review'\n ? `${truncateName(props.dictName, 20)} · ${t.practice.reviewLabel}`\n : `${truncateName(props.dictName, 20)} · ${t.practice.pause.chapter(props.chapterIndex + 1, props.totalChapters)}`;\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.warning}>\n {t.practice.pause.title}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n <Box marginTop={2}>\n <ProgressBar frac={frac} />\n </Box>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.pause.progress(props.completed, props.total)}</Text>\n </Box>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.pause.hint}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction ErrorView({ msg }: { msg: string }) {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.error}>{msg}</Text>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>Esc {t.common.back}</Text>\n </Box>\n <BackKey />\n </Box>\n );\n}\n\nfunction BackKey() {\n const nav = useNav();\n useInput((_input, key) => {\n if (key.escape) nav.back();\n });\n return null;\n}\n\nfunction Centered({ text, color }: { text: string; color: string }) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={color}>{text}</Text>\n </Box>\n );\n}\n\nfunction SummaryView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n summary: { wordCount: number; errors: number; durationMs: number; perWordErrors: Record<string, number> };\n}) {\n const { summary } = props;\n const minutes = summary.durationMs / 60000;\n const wpm = minutes > 0 ? Math.round((summary.wordCount / minutes) * 10) / 10 : 0;\n const errorWords = Object.keys(summary.perWordErrors).length;\n const acc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - errorWords) / summary.wordCount);\n const accPct = Math.round(acc * 1000) / 10;\n\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const name = truncateName(props.dictName, 20);\n const subtitle =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName}`;\n\n const nextLabel =\n props.mode === 'loop'\n ? t.practice.summary.loopAgain\n : props.mode === 'review' || props.chapterIndex + 1 >= props.totalChapters\n ? t.practice.summary.backMenu\n : t.practice.summary.nextChapter;\n const footer = `Enter ${nextLabel} · m ${t.practice.summary.reviewMistakes} · Esc ${t.practice.summary.backMenu}`;\n\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.success}>\n {t.practice.chapterComplete}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n\n <Box marginTop={3} flexDirection=\"row\" justifyContent=\"center\">\n <StatCard label={t.practice.statCards.words} value={String(summary.wordCount)} color={PALETTE.text} />\n <StatCard\n label={t.practice.statCards.errors}\n value={String(summary.errors)}\n color={summary.errors > 0 ? PALETTE.error : PALETTE.muted}\n />\n <StatCard label={t.practice.statCards.wpm} value={String(wpm)} color={PALETTE.accent} />\n <StatCard label={t.practice.statCards.accuracy} value={`${accPct}%`} color={PALETTE.accent} />\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.statCards.elapsed(fmtTime(summary.durationMs))}</Text>\n </Box>\n\n <Box flexGrow={1} />\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{footer}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction StatCard({ label, value, color }: { label: string; value: string; color: string }) {\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" marginX={3}>\n <Text bold color={color}>{value}</Text>\n <Text color={PALETTE.muted}>{label}</Text>\n </Box>\n );\n}\n","export function shuffle<T>(arr: readonly T[], rng: () => number = Math.random): T[] {\n const out = [...arr];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(rng() * (i + 1));\n const tmp = out[i]!;\n out[i] = out[j]!;\n out[j] = tmp;\n }\n return out;\n}\n\nexport function mulberry32(seed: number): () => number {\n let t = seed >>> 0;\n return () => {\n t = (t + 0x6d2b79f5) >>> 0;\n let r = Math.imul(t ^ (t >>> 15), 1 | t);\n r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;\n return ((r ^ (r >>> 14)) >>> 0) / 4294967296;\n };\n}\n","import type { Word } from './dictionary.js';\nimport { shuffle, mulberry32 } from '../util/shuffle.js';\n\nexport type Mode = 'order' | 'dictation' | 'review' | 'random' | 'loop';\n\nexport function chunkChapters(words: Word[], chapterSize: number): Word[][] {\n if (chapterSize <= 0) throw new Error('chapterSize must be positive');\n const chunks: Word[][] = [];\n for (let i = 0; i < words.length; i += chapterSize) {\n chunks.push(words.slice(i, i + chapterSize));\n }\n return chunks;\n}\n\nexport function chapterCount(totalWords: number, chapterSize: number): number {\n return Math.ceil(totalWords / chapterSize);\n}\n\n/**\n * Build a play list for a chapter, applying the given mode.\n * - order: return as-is\n * - dictation: same as order (hide-the-word behavior is UI-only)\n * - random: single shuffled pass\n * - loop: return as-is; the practice screen drives the repeat\n * - review: caller passes the mistake-book words, we just chunk those\n */\nexport function buildPlaylist(chapter: Word[], mode: Mode, seed?: number): Word[] {\n if (mode === 'random') {\n const rng = seed === undefined ? Math.random : mulberry32(seed);\n return shuffle(chapter, rng);\n }\n return chapter;\n}\n","export type InputState = {\n target: string;\n typed: string;\n errorsThisWord: number;\n};\n\nexport type InputEvent =\n | { type: 'char'; ch: string }\n | { type: 'backspace' }\n | { type: 'reset' };\n\nexport type InputEffect = 'none' | 'progress' | 'wrong' | 'correct' | 'skipped';\n\nexport function initialState(target: string): InputState {\n return { target, typed: '', errorsThisWord: 0 };\n}\n\nexport function reduce(state: InputState, ev: InputEvent): { state: InputState; effect: InputEffect } {\n switch (ev.type) {\n case 'reset':\n return { state: { ...state, typed: '' }, effect: 'none' };\n case 'backspace': {\n if (state.typed.length === 0) return { state, effect: 'none' };\n return { state: { ...state, typed: state.typed.slice(0, -1) }, effect: 'none' };\n }\n case 'char': {\n const candidate = state.typed + ev.ch;\n // Compare by code-point index, not byte length, to handle unicode safely.\n const targetUpToCandidate = [...state.target].slice(0, [...candidate].length).join('');\n if (candidate === targetUpToCandidate) {\n if (candidate.length === state.target.length) {\n return { state: { ...state, typed: candidate }, effect: 'correct' };\n }\n return { state: { ...state, typed: candidate }, effect: 'progress' };\n }\n return {\n state: { ...state, typed: '', errorsThisWord: state.errorsThisWord + 1 },\n effect: 'wrong',\n };\n }\n }\n}\n","import type { Word } from './dictionary.js';\nimport { initialState, reduce, type InputEvent, type InputState, type InputEffect } from './input-buffer.js';\n\nexport type SessionWordResult = { word: string; errors: number; durationMs: number; skipped?: boolean };\n\nexport type Session = {\n startedAt: number;\n results: SessionWordResult[];\n current: { wordIndex: number; wordStartedAt: number; input: InputState } | null;\n finishedAt: number | null;\n playlist: Word[];\n};\n\nexport function startSession(playlist: Word[], now = Date.now()): Session {\n if (playlist.length === 0) {\n return { startedAt: now, results: [], current: null, finishedAt: now, playlist };\n }\n return {\n startedAt: now,\n results: [],\n current: { wordIndex: 0, wordStartedAt: now, input: initialState(playlist[0]!.name) },\n finishedAt: null,\n playlist,\n };\n}\n\nexport function feedSession(session: Session, ev: InputEvent, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const { state, effect } = reduce(session.current.input, ev);\n if (effect === 'correct') {\n const finished: SessionWordResult = {\n word: state.target,\n errors: state.errorsThisWord,\n durationMs: now - session.current.wordStartedAt,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, finished];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect,\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect,\n };\n }\n return {\n session: {\n ...session,\n current: { ...session.current, input: state },\n },\n effect,\n };\n}\n\nexport function skipSession(session: Session, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const result: SessionWordResult = {\n word: session.current.input.target,\n errors: 0,\n durationMs: now - session.current.wordStartedAt,\n skipped: true,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, result];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect: 'skipped',\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect: 'skipped',\n };\n}\n\nexport function sessionSummary(session: Session): {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n} {\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const durationMs =\n (session.finishedAt ?? Date.now()) - session.startedAt;\n const perWordErrors: Record<string, number> = {};\n for (const r of session.results) {\n if (r.errors > 0) perWordErrors[r.word] = (perWordErrors[r.word] ?? 0) + r.errors;\n }\n return { wordCount: session.results.length, errors, durationMs, perWordErrors };\n}\n","import { useEffect, useReducer, useRef, useState } from 'react';\nimport { useInput, useApp } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport { startSession, feedSession, skipSession, type Session } from '../../domain/session.js';\nimport type { InputEffect } from '../../domain/input-buffer.js';\n\ntype Action =\n | { type: 'event'; input: string; key: { backspace?: boolean; delete?: boolean; tab?: boolean; escape?: boolean; return?: boolean; ctrl?: boolean }; now: number }\n | { type: 'start'; playlist: Word[]; now: number }\n | { type: 'skip'; now: number };\n\n// effectSeq is a monotonic counter bumped on every dispatch that resolves into\n// a new lastEffect value. Consumers (PracticeScreen audio effect) depend on it\n// instead of lastEffect alone because React's Object.is comparison treats two\n// consecutive 'progress' strings as equal — without a counter, useEffect would\n// fire on only the first char of a word, dropping the keystroke sound on every\n// subsequent character.\ntype LoopState = { session: Session; lastEffect: InputEffect | null; effectSeq: number };\n\nfunction reducer(state: LoopState, action: Action): LoopState {\n if (action.type === 'start') {\n return { session: startSession(action.playlist, action.now), lastEffect: null, effectSeq: 0 };\n }\n if (action.type === 'skip') {\n const r = skipSession(state.session, action.now);\n return { session: r.session, lastEffect: r.effect, effectSeq: state.effectSeq + 1 };\n }\n if (action.type === 'event') {\n if (action.key.backspace || action.key.delete) {\n const r = feedSession(state.session, { type: 'backspace' }, action.now);\n return { session: r.session, lastEffect: r.effect, effectSeq: state.effectSeq + 1 };\n }\n if (action.input.length === 0) return state;\n let session = state.session;\n let lastEffect: InputEffect | null = state.lastEffect;\n for (const c of action.input) {\n const r = feedSession(session, { type: 'char', ch: c }, action.now);\n session = r.session;\n lastEffect = r.effect;\n if (session.finishedAt !== null) break;\n }\n return { session, lastEffect, effectSeq: state.effectSeq + 1 };\n }\n return state;\n}\n\n// Classify a typed batch from Ink's useInput. Single source of truth so the\n// IME-detection logic stays unit-testable without rendering Ink.\n// kind='ime' → contains at least one non-ASCII codepoint (>= 0x80, e.g.\n// CJK / Latin-1 supplement); reject the batch and show hint.\n// kind='valid' → all-ASCII printable; `cleaned` has the dispatchable subset.\n// kind='noise' → ASCII control (DEL, NUL, etc.) or empty; ignore silently.\nexport type InputBatchKind = 'ime' | 'valid' | 'noise';\nexport function classifyInputBatch(input: string): { kind: InputBatchKind; cleaned: string } {\n const chars = [...input];\n if (chars.some((c) => c.codePointAt(0)! >= 0x80)) {\n return { kind: 'ime', cleaned: '' };\n }\n const cleaned = chars\n .filter((c) => {\n const cp = c.codePointAt(0)!;\n return cp >= 0x20 && cp <= 0x7e;\n })\n .join('');\n return { kind: cleaned.length > 0 ? 'valid' : 'noise', cleaned };\n}\n\nexport type UseWordLoopOpts = {\n playlist: Word[];\n onComplete: (session: Session) => void;\n onTab?: () => void;\n onEscape?: () => void;\n onSkip?: () => void;\n // Fires when the typed batch contains any codepoint > 0x7E — almost always\n // means the user's IME is active (CJK input method). The batch is rejected\n // entirely so the wrong characters aren't fed to the session.\n onImeBlock?: () => void;\n // Fires when a non-empty all-ASCII batch is about to dispatch. Used to\n // clear a previously-set IME warning so it auto-dismisses on the first\n // successful keystroke.\n onValidInput?: () => void;\n enabled?: boolean;\n};\n\nexport function useWordLoop({\n playlist,\n onComplete,\n onTab,\n onEscape,\n onSkip,\n onImeBlock,\n onValidInput,\n enabled = true,\n}: UseWordLoopOpts) {\n const [state, dispatch] = useReducer(reducer, undefined, () => ({\n session: startSession(playlist, Date.now()),\n lastEffect: null as InputEffect | null,\n effectSeq: 0,\n }));\n const completedRef = useRef(false);\n const [tick, setTick] = useState(0);\n const { exit } = useApp();\n\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n exit();\n return;\n }\n if (key.ctrl && input === 'n') {\n onSkip?.();\n dispatch({ type: 'skip', now: Date.now() });\n return;\n }\n if (key.escape) {\n onEscape?.();\n return;\n }\n if (key.tab) {\n onTab?.();\n return;\n }\n if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return) return;\n if (key.ctrl || key.meta) return;\n const { kind, cleaned } = classifyInputBatch(input);\n if (kind === 'ime') {\n onImeBlock?.();\n return;\n }\n if (kind === 'noise') return;\n onValidInput?.();\n dispatch({ type: 'event', input: cleaned, key, now: Date.now() });\n },\n { isActive: enabled },\n );\n\n useEffect(() => {\n if (state.session.finishedAt !== null && !completedRef.current) {\n completedRef.current = true;\n onComplete(state.session);\n }\n }, [state.session, onComplete]);\n\n useEffect(() => {\n if (state.session.finishedAt !== null) return;\n const id = setInterval(() => setTick((t) => t + 1), 1000);\n return () => clearInterval(id);\n }, [state.session.finishedAt]);\n\n return { session: state.session, lastEffect: state.lastEffect, effectSeq: state.effectSeq, tick };\n}\n\n// Exposed for unit tests only — verifies effectSeq monotonic increment and\n// effect dispatch behavior without mounting a full Ink tree.\nexport const __test = { reducer };\n","import { useEffect, useRef } from 'react';\nimport {\n initAudio,\n playCorrect,\n playWrong,\n playKeystroke,\n playPronunciation,\n prefetchPronunciation,\n} from '../../infra/audio.js';\n\ntype Opts = {\n enabled: boolean;\n accent: 'us' | 'uk';\n autoplayPronunciation: boolean;\n pronunciationRate: number;\n pronunciationSource: 'youdao' | 'dictapi';\n};\n\nexport type AudioControls = {\n keystroke: () => void;\n correct: () => void;\n wrong: () => void;\n pronounce: (word: string) => void;\n prefetch: (word: string) => void;\n};\n\nexport function useAudio(opts: Opts): AudioControls {\n const initedRef = useRef(false);\n useEffect(() => {\n if (initedRef.current) return;\n initedRef.current = true;\n initAudio(!opts.enabled).catch(() => undefined);\n }, [opts.enabled]);\n\n return {\n keystroke: () => opts.enabled && playKeystroke(),\n correct: () => opts.enabled && playCorrect(),\n wrong: () => opts.enabled && playWrong(),\n pronounce: (word) => {\n if (!opts.enabled) return;\n if (opts.autoplayPronunciation)\n void playPronunciation(word, opts.accent, opts.pronunciationRate, opts.pronunciationSource);\n },\n prefetch: (word) => {\n if (!opts.enabled) return;\n void prefetchPronunciation(word, opts.accent, opts.pronunciationSource);\n },\n };\n}\n","import { useCallback } from 'react';\nimport { appendSession, type SessionRecord } from '../../domain/stats.js';\nimport { loadMistakes, saveMistakes, bump } from '../../domain/mistakes.js';\nimport { addChapter as trackChapter } from '../../infra/session-tracker.js';\nimport type { Mode } from '../../domain/chapters.js';\n\ntype Summary = {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n};\n\nexport function useSessionPersistence(meta: { dictId: string; chapterIndex: number; mode: Mode }) {\n return useCallback(\n async (summary: Summary): Promise<void> => {\n const rec: SessionRecord = {\n ts: new Date().toISOString(),\n dictId: meta.dictId,\n chapter: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n };\n await appendSession(rec);\n trackChapter({\n dictId: meta.dictId,\n chapterIndex: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n });\n const dirty = Object.entries(summary.perWordErrors).filter(([, n]) => n > 0);\n if (dirty.length === 0) return;\n let book = await loadMistakes();\n for (const [word, n] of dirty) book = bump(book, word, meta.dictId, n);\n await saveMistakes(book);\n },\n [meta.dictId, meta.chapterIndex, meta.mode],\n );\n}\n","// Detect terminal capabilities relevant to BigWord rendering.\n//\n// VT100 DECDWL/DECDHL (ESC#3 / ESC#4 / ESC#6) let us render the practice\n// word using the terminal's real font at 2× width and 2× height. Support\n// varies: macOS Terminal.app, Windows Terminal, iTerm2, WezTerm, xterm,\n// mintty, Alacritty render correctly; kitty / VTE-based / Konsole either\n// skip the attribute or render glitchy output.\n//\n// Detection is env-only — runtime device-attribute queries (ESC[c) would\n// race against Ink's input pipeline and aren't worth the complexity.\n\nexport type TermCaps = {\n supportsDoubleHeight: boolean;\n};\n\nexport function detectTermCaps(env: NodeJS.ProcessEnv = process.env): TermCaps {\n if (env.KITTY_WINDOW_ID) return { supportsDoubleHeight: false };\n if (env.VTE_VERSION) return { supportsDoubleHeight: false };\n if (env.KONSOLE_VERSION) return { supportsDoubleHeight: false };\n\n if (env.WT_SESSION) return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'Apple_Terminal') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'iTerm.app') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'WezTerm') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'mintty') return { supportsDoubleHeight: true };\n if (env.ALACRITTY_SOCKET || env.ALACRITTY_LOG) return { supportsDoubleHeight: true };\n if (env.XTERM_VERSION) return { supportsDoubleHeight: true };\n\n return { supportsDoubleHeight: false };\n}\n\nlet cachedCaps: TermCaps | null = null;\nexport function getTermCaps(): TermCaps {\n if (!cachedCaps) cachedCaps = detectTermCaps();\n return cachedCaps;\n}\n\nexport type WordDisplay = 'auto' | 'huge' | 'standard';\n\n// Decide whether the practice word should render via DECDHL (2×2 huge).\n// `huge` forces it (subject to fitting the width); `auto` defers to terminal\n// detection; `standard` never. The fit guard keeps a long word from wrapping\n// at 2× width on a narrow terminal — fall back to single-cell instead.\nexport function shouldUseHuge(\n wordDisplay: WordDisplay,\n targetLength: number,\n cols: number,\n caps: TermCaps = getTermCaps(),\n): boolean {\n if (wordDisplay === 'standard') return false;\n const supported = wordDisplay === 'huge' || caps.supportsDoubleHeight;\n if (!supported) return false;\n return cols >= 2 * targetLength + 4;\n}\n","import { Box, Text, Transform, useStdout } from 'ink';\nimport { PALETTE } from './BigWord.js';\n\n// Renders the practice word using VT100 DECDHL (double-height + double-width\n// line attribute). The terminal draws the SAME glyphs as a single-cell line\n// but at 2× scale — real terminal font, no rasterization, no figlet/ASCII art.\n//\n// DECDHL needs two physical rows with identical content: ESC#3 (top half) and\n// ESC#4 (bottom half); the terminal merges them into one visual 2×-tall row.\n//\n// Why <Transform> and not a raw-escape string inside <Text>: Ink's text\n// pipeline (slice-ansi / wrap-ansi) only understands SGR escapes. A bare\n// `\\x1b#3` desyncs its parser and leaks fragments of the following color\n// codes as visible characters (e.g. \"magma\" → \"m8ma8mm8ma\"). <Transform>\n// runs AFTER layout/slicing, so it prepends the line attribute to the\n// already-composed line without corrupting it, and Ink still handles the\n// per-glyph SGR coloring natively.\n//\n// Centering: the whole physical line is doubled, including leading pad\n// spaces, so actual pad chars = visual_left_margin / 2. We must use\n// width=\"100%\" so a centering parent (alignItems=\"center\") doesn't insert\n// its own single-width pad *before* the line attribute — that pad would also\n// double and shove the word off the right edge.\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n align?: 'center' | 'left';\n};\n\nexport function BigWordHuge({\n target,\n typed,\n error = false,\n hideTarget = false,\n align = 'center',\n}: Props) {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n const chars = [...target];\n const typedChars = [...typed];\n\n const visualPad =\n align === 'center' ? Math.max(0, Math.floor((cols - chars.length * 2) / 2)) : 0;\n const pad = ' '.repeat(Math.floor(visualPad / 2));\n\n const glyphs = () =>\n chars.map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n });\n\n return (\n <Box\n flexDirection=\"column\"\n width={align === 'center' ? '100%' : undefined}\n paddingY={align === 'center' ? 2 : 0}\n >\n <Transform transform={(line) => `\\x1b#3${line}`}>\n <Text>\n {pad}\n {glyphs()}\n </Text>\n </Transform>\n <Transform transform={(line) => `\\x1b#4${line}`}>\n <Text>\n {pad}\n {glyphs()}\n </Text>\n </Transform>\n </Box>\n );\n}\n","import { useAppState } from '../app-state.js';\nimport { shouldUseHuge } from '../../util/term-caps.js';\nimport { BigWord } from './BigWord.js';\nimport { BigWordHuge } from './BigWordHuge.js';\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nexport function BigWordAuto(props: Props) {\n const { cfg } = useAppState();\n const cols = process.stdout?.columns ?? 80;\n const huge = shouldUseHuge(cfg.wordDisplay, [...props.target].length, cols);\n return huge ? <BigWordHuge {...props} /> : <BigWord {...props} />;\n}\n","import type { ReactNode } from 'react';\nimport { Box, Text, useStdout } from 'ink';\nimport { PALETTE } from '../components/BigWord.js';\nimport { BigWordHuge } from '../components/BigWordHuge.js';\nimport { useStrings } from '../../i18n/context.js';\n\nconst RIGHT_WIDTH = 28;\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction useLeftWidth(): number {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n return Math.max(20, cols - RIGHT_WIDTH);\n}\n\nfunction Row({ left, right }: { left: ReactNode; right: ReactNode }) {\n const leftWidth = useLeftWidth();\n return (\n <Box>\n <Box width={leftWidth}>{left}</Box>\n <Box width={RIGHT_WIDTH} justifyContent=\"flex-end\">\n {right}\n </Box>\n </Box>\n );\n}\n\nexport function StealthTyping(props: {\n target: string;\n typed: string;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n error: boolean;\n huge: boolean;\n imeBlocked: boolean;\n audioWarning: string | null;\n info: {\n visible: boolean;\n dictName: string;\n chapterLabel: string;\n completed: number;\n total: number;\n wpm: number;\n accPct: number;\n elapsedMs: number;\n };\n}) {\n const t = useStrings();\n const target = [...props.target];\n const typed = [...props.typed];\n\n // Row 1: word chars + inline phonetic (two-space gap, italic + dim muted).\n // error=true flashes the entire word red; phonetic remains dim regardless.\n const wordCell = (\n <Box>\n {target.map((ch, i) => {\n const isTyped = i < typed.length;\n const display = props.hideTarget && !isTyped ? '_' : isTyped ? typed[i]! : ch;\n const color = props.error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n {props.phonetic && (\n <>\n <Text> </Text>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </>\n )}\n </Box>\n );\n\n // Row 3: first translation in primary cyan (matches TypingLayout translation color)\n const translationCell = props.translation.length > 0 ? (\n <Text color={PALETTE.primary}>{props.translation[0]!}</Text>\n ) : (\n <Text> </Text>\n );\n\n // In huge mode the word lives on its own two DECDHL lines, so the phonetic\n // (normally inline after the word) gets its own row below.\n const phoneticCell = props.phonetic ? (\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n ) : (\n <Text> </Text>\n );\n\n const info = props.info;\n const accFmt = Number.isInteger(info.accPct) ? `${info.accPct}` : info.accPct.toFixed(1);\n\n // Right column priority: imeBlocked > audioWarning > info > idle.\n // imeBlocked: warning indicator on row 1, rows 2/3 blank.\n // audioWarning (and no IME issue): `! audio` short marker on row 1, rows 2/3 blank.\n // info.visible: dict / progress / time on rows 1/2/3.\n // idle: all three rows blank.\n const showAudio = !props.imeBlocked && props.audioWarning !== null;\n const right1 = props.imeBlocked ? (\n <Text color={PALETTE.warning}>{t.practice.imeWarningShort}</Text>\n ) : showAudio ? (\n <Text color={PALETTE.warning}>{t.practice.audioWarningShort}</Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.dictName} · ${info.chapterLabel}`}</Text>\n ) : (\n <Text> </Text>\n );\n const right2 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.completed}/${info.total} · ${info.wpm}wpm · ${accFmt}%`}</Text>\n ) : (\n <Text> </Text>\n );\n const right3 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{fmtTime(info.elapsedMs)}</Text>\n ) : (\n <Text> </Text>\n );\n\n // Huge layout: the word occupies its own two DECDHL lines (full physical\n // lines — nothing on the right, or the line attribute would double the info\n // chips too). Phonetic / translation / time move to the three rows below,\n // preserving every info slot.\n if (props.huge) {\n return (\n <Box flexDirection=\"column\">\n <BigWordHuge\n target={props.target}\n typed={props.typed}\n error={props.error}\n hideTarget={props.hideTarget}\n align=\"left\"\n />\n <Row left={phoneticCell} right={right1} />\n <Row left={translationCell} right={right2} />\n <Row left={<Text> </Text>} right={right3} />\n </Box>\n );\n }\n\n // Layout: 3 rendered rows. Middle row's left is blank → acts as a 1-line\n // visual gap between word+phonetic and translation. Right column keeps all\n // three info slots (dict / progress / time) when info.visible is true.\n return (\n <Box flexDirection=\"column\">\n <Row left={wordCell} right={right1} />\n <Row left={<Text> </Text>} right={right2} />\n <Row left={translationCell} right={right3} />\n </Box>\n );\n}\n\nexport function StealthPaused() {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.warning}>{t.stealth.paused}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.pausedHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n\nexport function StealthSummary(props: {\n wordCount: number;\n errors: number;\n durationMs: number;\n wpm: number;\n accPct: number;\n}) {\n const t = useStrings();\n const accFmt = Number.isInteger(props.accPct) ? `${props.accPct}` : props.accPct.toFixed(1);\n const line = `${t.stealth.chapterDone} · ${props.wordCount}w · ${props.wpm}wpm · ${accFmt}% · ${fmtTime(props.durationMs)}`;\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.success}>{line}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.nextHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n"],"mappings":"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","names":["useState","useEffect","useRef","Box","Text","useApp","useInput","shuffle","arr","rng","out","i","j","tmp","mulberry32","seed","t","chunkChapters","words","chapterSize","chunks","i","buildPlaylist","chapter","mode","seed","rng","mulberry32","shuffle","initialState","target","reduce","state","ev","candidate","targetUpToCandidate","startSession","playlist","now","initialState","feedSession","session","ev","state","effect","reduce","finished","nextIndex","results","skipSession","result","sessionSummary","errors","a","r","durationMs","perWordErrors","useEffect","useReducer","useRef","useState","useInput","useApp","reducer","state","action","startSession","skipSession","r","feedSession","session","lastEffect","c","classifyInputBatch","input","chars","cleaned","cp","useWordLoop","playlist","onComplete","onTab","onEscape","onSkip","onImeBlock","onValidInput","enabled","dispatch","useReducer","completedRef","useRef","tick","setTick","useState","exit","useApp","useInput","key","kind","useEffect","id","t","useEffect","useRef","useAudio","opts","initedRef","useRef","useEffect","initAudio","playKeystroke","playCorrect","playWrong","word","playPronunciation","prefetchPronunciation","useCallback","useSessionPersistence","meta","useCallback","summary","rec","appendSession","addChapter","dirty","n","book","loadMistakes","word","bump","saveMistakes","detectTermCaps","env","cachedCaps","getTermCaps","shouldUseHuge","wordDisplay","targetLength","cols","caps","Box","Text","Transform","useStdout","jsx","jsxs","BigWordHuge","target","typed","error","hideTarget","align","stdout","useStdout","cols","chars","typedChars","visualPad","pad","glyphs","ch","i","isTyped","display","color","PALETTE","Text","Box","Transform","line","jsx","BigWordAuto","props","cfg","useAppState","cols","shouldUseHuge","BigWordHuge","BigWord","Box","Text","useStdout","Fragment","jsx","jsxs","RIGHT_WIDTH","fmtTime","ms","total","m","s","useLeftWidth","stdout","useStdout","cols","Row","left","right","leftWidth","Box","StealthTyping","props","t","useStrings","target","typed","wordCell","ch","i","isTyped","display","color","PALETTE","Text","translationCell","phoneticCell","info","accFmt","showAudio","right1","right2","right3","BigWordHuge","StealthPaused","StealthSummary","line","jsx","jsxs","PracticeScreen","params","dictId","chapterIndex","mode","cfg","useAppState","t","useStrings","phase","setPhase","useState","loaded","setLoaded","errorMsg","setErrorMsg","useEffect","cancelled","words","ensureDictionary","book","loadMistakes","reviewWords","w","chapters","chunkChapters","idx","playlist","buildPlaylist","err","Centered","PALETTE","ErrorView","PracticeRunner","stealth","nav","useNav","exit","useApp","goBack","persist","useSessionPersistence","dictName","useDictName","audio","useAudio","audioStatus","useAudioStatus","audioWarn","finishedRef","useRef","lastEffectSeqRef","lastIndexRef","infoVisible","setInfoVisible","infoShownAt","setInfoShownAt","imeBlocked","setImeBlocked","id","session","lastEffect","effectSeq","tick","useWordLoop","s","sessionSummary","cur","next","useInput","_input","key","input","setSilentExit","nextIdx","completed","errors","a","r","elapsedMs","minutes","wpm","summary","StealthPaused","sMinutes","sWpm","sErrWords","sAcc","sAccPct","StealthSummary","currentWord","inputState","errWords","accFrac","accPct","chapterLabel","StealthTyping","pickPhonetic","shouldUseHuge","truncateName","PausedView","SummaryView","TypingLayout","word","accent","fmtTime","ms","total","m","props","progressFrac","Box","StatusBar","BigWordAuto","Text","tr","i","ProgressBar","modeName","accentName","name","left","right","frac","cols","width","filled","empty","subtitle","msg","BackKey","text","color","errorWords","acc","footer","StatCard","label","value"]}
@@ -1,3 +1,3 @@
1
- import{a as L,b as R,c as I}from"./chunk-6ROGUGNX.js";import{a as C,b as k}from"./chunk-MFGIEKBU.js";import{b as $,e as z}from"./chunk-GULN5HRV.js";import{a as B,b as D,c as w,e as H}from"./chunk-QG7ZTS2G.js";import{a as P,b as M,c as E,d as N,f as a}from"./chunk-VIOZNKSK.js";var _="\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H",F="\x1B[?25h\x1B[?1049l",v=!1,W=!1;function O(){if(W)return;W=!0;let e=()=>{if(v){try{process.stdout.write(F)}catch{}v=!1}};process.once("exit",e),process.once("SIGINT",()=>{e(),process.exit(130)}),process.once("SIGTERM",()=>{e(),process.exit(143)})}function Y(){v||process.stdout.isTTY&&process.env.QWERTY_NO_ALTSCREEN!=="1"&&(O(),process.stdout.write(_),v=!0)}function A(){if(v){try{process.stdout.write(F)}catch{}v=!1}}import{Suspense as ne,lazy as T,useRef as ie}from"react";import{Box as se,Text as ae,useApp as ce,useInput as ue}from"ink";import{useEffect as Q,useState as X}from"react";import{Box as U,useStdout as Z}from"ink";import{jsx as ee}from"react/jsx-runtime";function j({children:e}){let{stdout:t}=Z(),[s,r]=X(()=>({rows:t?.rows??24,cols:t?.columns??80}));return Q(()=>{Y();let n=()=>{r({rows:process.stdout.rows??24,cols:process.stdout.columns??80})};return process.stdout.on("resize",n),()=>{process.stdout.off("resize",n),A()}},[]),ee(U,{width:s.cols,height:s.rows,flexDirection:"column",children:e})}import{useState as te}from"react";import{Box as y,Text as f,useApp as re,useInput as oe}from"ink";import{jsx as g,jsxs as x}from"react/jsx-runtime";function q({cfg:e}){let[t,s]=te(0),{exit:r}=re(),n=M(),h=R(),p=N(),l=D(e.defaultDict),o=p.mainMenu.items,S=async c=>{if(!e.defaultDict){n.navigate({name:"dict",params:{pickerMode:"choose-then-practice"}});return}let m=z(await $(),e.defaultDict);if(c){I({type:"stealth",dictId:e.defaultDict,chapterIndex:m,mode:e.defaultMode}),r();return}n.navigate({name:"practice",params:{dictId:e.defaultDict,chapterIndex:m,mode:e.defaultMode,stealth:c}})},d=[{key:"p",label:o.practiceLabel,hint:e.defaultDict?o.practiceHintWith(H(l,24)):o.practiceHintNone,run:()=>{S(e.stealth==="default")}}];(e.stealth==="menu"||e.stealth==="default")&&d.push({key:"b",label:o.stealthLabel,hint:o.stealthHint,run:()=>{S(!0)}}),d.push({key:"d",label:o.dictLabel,hint:o.dictHint,run:()=>n.navigate({name:"dict"})},{key:"w",label:o.wordLabel,hint:o.wordHint,run:()=>n.navigate({name:"word"})},{key:"s",label:o.statsLabel,hint:o.statsHint,run:()=>n.navigate({name:"stats"})},{key:"c",label:o.configLabel,hint:o.configHint,run:()=>n.navigate({name:"config"})},{key:"q",label:o.quitLabel,hint:o.quitHint,run:()=>r()});let J=Math.max(...d.map(c=>w(c.label)))+4;return oe((c,m)=>{if(m.escape){r();return}if(m.upArrow&&s(u=>(u-1+d.length)%d.length),m.downArrow&&s(u=>(u+1)%d.length),m.return){d[t].run();return}if(c==="?"){n.navigate({name:"help"});return}for(let u of d)if(c===u.key){u.run();return}}),x(y,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",children:[e.stealth!=="default"&&x(y,{children:[g(f,{bold:!0,color:a.accent,children:p.app.title}),x(f,{color:a.muted,children:[" \xB7 ",p.app.subtitle]})]}),g(y,{marginTop:e.stealth==="default"?0:2,flexDirection:"column",children:d.map((c,m)=>{let u=m===t,K=" ".repeat(Math.max(0,J-w(c.label)));return x(y,{children:[g(f,{color:u?a.accent:a.muted,children:u?"\u258C ":" "}),x(f,{color:u?a.accent:a.muted,children:["[",c.key,"]"]}),g(f,{children:" "}),x(f,{bold:u,color:u?a.text:a.muted,children:[c.label,K]}),g(f,{color:a.muted,children:c.hint})]},c.key)})}),g(y,{marginTop:2,children:x(f,{color:a.muted,children:[p.mainMenu.hint," \xB7 ",p.mainMenu.helpHint]})}),h.warning&&g(y,{marginTop:1,children:g(f,{color:a.warning,children:p.audio.noPlayer})})]})}import{jsx as i}from"react/jsx-runtime";var pe=T(()=>import("./PracticeScreen-PXPBME6H.js").then(e=>({default:e.PracticeScreen}))),le=T(()=>import("./DictBrowser-I6AEYY2I.js").then(e=>({default:e.DictBrowser}))),de=T(()=>import("./ConfigEditor-YYZQ7TUJ.js").then(e=>({default:e.ConfigEditor}))),me=T(()=>import("./StatsViewer-JIFWFPDL.js").then(e=>({default:e.StatsViewer}))),fe=T(()=>import("./WordLookup-6JFXQYSQ.js").then(e=>({default:e.WordLookup}))),he=T(()=>import("./HelpScreen-IKHDZGTG.js").then(e=>({default:e.HelpScreen})));function ge(){return i(se,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(ae,{color:a.muted,children:"\u2026"})})}function Xe({initial:e,initialCfg:t,inline:s=!1}){return i(C,{initialCfg:t,children:i(xe,{children:i(B,{children:i(L,{disabled:!t.sounds.master,children:i(P,{initial:e,children:s?i(V,{inline:!0}):i(j,{children:i(V,{})})})})})})})}function xe({children:e}){let{cfg:t}=k();return i(E,{pref:t.language,children:e})}function be(e){if(e.name==="practice"){let t=e.params;return`practice:${t.dictId}:${t.chapterIndex}:${t.mode}:${t.stealth?"s":"n"}`}return e.name}function V({inline:e=!1}){let t=M(),{cfg:s}=k(),{exit:r}=ce(),n=ie(null);ue((o,S)=>{S.ctrl&&o==="c"&&r()});let h=t.current,p=be(h);n.current!==p&&(!e&&process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[H"),n.current=p);let l=(()=>{switch(h.name){case"main":return i(q,{cfg:s});case"practice":return i(pe,{params:h.params});case"dict":return i(le,{params:h.params});case"config":return i(de,{});case"stats":return i(me,{});case"word":return i(fe,{});case"help":return i(he,{})}})();return i(ne,{fallback:i(ge,{}),children:l})}import b from"chalk";import Se from"boxen";function nt(){A()}function G(e,t){let s=Math.floor(e/1e3),r=Math.floor(s/60),n=s%60;return t==="zh"?r===0?`${n} \u79D2`:`${r} \u5206 ${n} \u79D2`:r===0?`${n}s`:`${r}m ${n}s`}function we(e){return e>=90?b.green:e<75?b.dim:t=>t}function ve(e,t,s){let r=[],n=[t.report.duration];e.chaptersCompleted===0?n.push(t.report.notPracticed):(n.push(t.report.practiced,t.report.chapters,t.report.words,t.report.accuracy,t.report.wpm),e.newMistakeWords>0&&n.push(t.report.newMistakes));let h=Math.max(...n.map(w)),p=o=>o+" ".repeat(Math.max(0,h-w(o))),l=(o,S)=>`${b.dim(p(o))} ${S}`;if(r.push(l(t.report.duration,G(e.totalDurationMs,s))),e.chaptersCompleted===0)r.push(b.dim(t.report.notPracticed));else{r.push(l(t.report.practiced,G(e.practiceMs,s))),r.push(l(t.report.chapters,String(e.chaptersCompleted))),r.push(l(t.report.words,String(e.wordCount)));let o=Math.round(e.accuracy*1e3)/10;r.push(l(t.report.accuracy,we(o)(`${o}%`))),r.push(l(t.report.wpm,String(e.wpm))),e.newMistakeWords>0&&r.push(l(t.report.newMistakes,b.red(String(e.newMistakeWords))))}return r.push(""),r.push(b.dim.italic(t.report.farewell)),r}function it(e,t,s){if(e.startedAt===null&&e.chaptersCompleted===0)return;let r=ve(e,t,s).join(`
1
+ import{a as L,b as R,c as I}from"./chunk-6ROGUGNX.js";import{a as C,b as k}from"./chunk-MFGIEKBU.js";import{b as $,e as z}from"./chunk-GULN5HRV.js";import{a as B,b as D,c as w,e as H}from"./chunk-QG7ZTS2G.js";import{a as P,b as M,c as E,d as N,f as a}from"./chunk-VIOZNKSK.js";var _="\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H",F="\x1B[?25h\x1B[?1049l",v=!1,W=!1;function O(){if(W)return;W=!0;let e=()=>{if(v){try{process.stdout.write(F)}catch{}v=!1}};process.once("exit",e),process.once("SIGINT",()=>{e(),process.exit(130)}),process.once("SIGTERM",()=>{e(),process.exit(143)})}function Y(){v||process.stdout.isTTY&&process.env.QWERTY_NO_ALTSCREEN!=="1"&&(O(),process.stdout.write(_),v=!0)}function A(){if(v){try{process.stdout.write(F)}catch{}v=!1}}import{Suspense as ne,lazy as T,useRef as ie}from"react";import{Box as se,Text as ae,useApp as ce,useInput as ue}from"ink";import{useEffect as Q,useState as X}from"react";import{Box as U,useStdout as Z}from"ink";import{jsx as ee}from"react/jsx-runtime";function j({children:e}){let{stdout:t}=Z(),[s,r]=X(()=>({rows:t?.rows??24,cols:t?.columns??80}));return Q(()=>{Y();let n=()=>{r({rows:process.stdout.rows??24,cols:process.stdout.columns??80})};return process.stdout.on("resize",n),()=>{process.stdout.off("resize",n),A()}},[]),ee(U,{width:s.cols,height:s.rows,flexDirection:"column",children:e})}import{useState as te}from"react";import{Box as y,Text as f,useApp as re,useInput as oe}from"ink";import{jsx as g,jsxs as x}from"react/jsx-runtime";function q({cfg:e}){let[t,s]=te(0),{exit:r}=re(),n=M(),h=R(),p=N(),l=D(e.defaultDict),o=p.mainMenu.items,S=async c=>{if(!e.defaultDict){n.navigate({name:"dict",params:{pickerMode:"choose-then-practice"}});return}let m=z(await $(),e.defaultDict);if(c){I({type:"stealth",dictId:e.defaultDict,chapterIndex:m,mode:e.defaultMode}),r();return}n.navigate({name:"practice",params:{dictId:e.defaultDict,chapterIndex:m,mode:e.defaultMode,stealth:c}})},d=[{key:"p",label:o.practiceLabel,hint:e.defaultDict?o.practiceHintWith(H(l,24)):o.practiceHintNone,run:()=>{S(e.stealth==="default")}}];(e.stealth==="menu"||e.stealth==="default")&&d.push({key:"b",label:o.stealthLabel,hint:o.stealthHint,run:()=>{S(!0)}}),d.push({key:"d",label:o.dictLabel,hint:o.dictHint,run:()=>n.navigate({name:"dict"})},{key:"w",label:o.wordLabel,hint:o.wordHint,run:()=>n.navigate({name:"word"})},{key:"s",label:o.statsLabel,hint:o.statsHint,run:()=>n.navigate({name:"stats"})},{key:"c",label:o.configLabel,hint:o.configHint,run:()=>n.navigate({name:"config"})},{key:"q",label:o.quitLabel,hint:o.quitHint,run:()=>r()});let J=Math.max(...d.map(c=>w(c.label)))+4;return oe((c,m)=>{if(m.escape){r();return}if(m.upArrow&&s(u=>(u-1+d.length)%d.length),m.downArrow&&s(u=>(u+1)%d.length),m.return){d[t].run();return}if(c==="?"){n.navigate({name:"help"});return}for(let u of d)if(c===u.key){u.run();return}}),x(y,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",children:[e.stealth!=="default"&&x(y,{children:[g(f,{bold:!0,color:a.accent,children:p.app.title}),x(f,{color:a.muted,children:[" \xB7 ",p.app.subtitle]})]}),g(y,{marginTop:e.stealth==="default"?0:2,flexDirection:"column",children:d.map((c,m)=>{let u=m===t,K=" ".repeat(Math.max(0,J-w(c.label)));return x(y,{children:[g(f,{color:u?a.accent:a.muted,children:u?"\u258C ":" "}),x(f,{color:u?a.accent:a.muted,children:["[",c.key,"]"]}),g(f,{children:" "}),x(f,{bold:u,color:u?a.text:a.muted,children:[c.label,K]}),g(f,{color:a.muted,children:c.hint})]},c.key)})}),g(y,{marginTop:2,children:x(f,{color:a.muted,children:[p.mainMenu.hint," \xB7 ",p.mainMenu.helpHint]})}),h.warning&&g(y,{marginTop:1,children:g(f,{color:a.warning,children:p.audio.noPlayer})})]})}import{jsx as i}from"react/jsx-runtime";var pe=T(()=>import("./PracticeScreen-4GUFONK5.js").then(e=>({default:e.PracticeScreen}))),le=T(()=>import("./DictBrowser-I6AEYY2I.js").then(e=>({default:e.DictBrowser}))),de=T(()=>import("./ConfigEditor-YYZQ7TUJ.js").then(e=>({default:e.ConfigEditor}))),me=T(()=>import("./StatsViewer-JIFWFPDL.js").then(e=>({default:e.StatsViewer}))),fe=T(()=>import("./WordLookup-6JFXQYSQ.js").then(e=>({default:e.WordLookup}))),he=T(()=>import("./HelpScreen-IKHDZGTG.js").then(e=>({default:e.HelpScreen})));function ge(){return i(se,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(ae,{color:a.muted,children:"\u2026"})})}function Xe({initial:e,initialCfg:t,inline:s=!1}){return i(C,{initialCfg:t,children:i(xe,{children:i(B,{children:i(L,{disabled:!t.sounds.master,children:i(P,{initial:e,children:s?i(V,{inline:!0}):i(j,{children:i(V,{})})})})})})})}function xe({children:e}){let{cfg:t}=k();return i(E,{pref:t.language,children:e})}function be(e){if(e.name==="practice"){let t=e.params;return`practice:${t.dictId}:${t.chapterIndex}:${t.mode}:${t.stealth?"s":"n"}`}return e.name}function V({inline:e=!1}){let t=M(),{cfg:s}=k(),{exit:r}=ce(),n=ie(null);ue((o,S)=>{S.ctrl&&o==="c"&&r()});let h=t.current,p=be(h);n.current!==p&&(!e&&process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[H"),n.current=p);let l=(()=>{switch(h.name){case"main":return i(q,{cfg:s});case"practice":return i(pe,{params:h.params});case"dict":return i(le,{params:h.params});case"config":return i(de,{});case"stats":return i(me,{});case"word":return i(fe,{});case"help":return i(he,{})}})();return i(ne,{fallback:i(ge,{}),children:l})}import b from"chalk";import Se from"boxen";function nt(){A()}function G(e,t){let s=Math.floor(e/1e3),r=Math.floor(s/60),n=s%60;return t==="zh"?r===0?`${n} \u79D2`:`${r} \u5206 ${n} \u79D2`:r===0?`${n}s`:`${r}m ${n}s`}function we(e){return e>=90?b.green:e<75?b.dim:t=>t}function ve(e,t,s){let r=[],n=[t.report.duration];e.chaptersCompleted===0?n.push(t.report.notPracticed):(n.push(t.report.practiced,t.report.chapters,t.report.words,t.report.accuracy,t.report.wpm),e.newMistakeWords>0&&n.push(t.report.newMistakes));let h=Math.max(...n.map(w)),p=o=>o+" ".repeat(Math.max(0,h-w(o))),l=(o,S)=>`${b.dim(p(o))} ${S}`;if(r.push(l(t.report.duration,G(e.totalDurationMs,s))),e.chaptersCompleted===0)r.push(b.dim(t.report.notPracticed));else{r.push(l(t.report.practiced,G(e.practiceMs,s))),r.push(l(t.report.chapters,String(e.chaptersCompleted))),r.push(l(t.report.words,String(e.wordCount)));let o=Math.round(e.accuracy*1e3)/10;r.push(l(t.report.accuracy,we(o)(`${o}%`))),r.push(l(t.report.wpm,String(e.wpm))),e.newMistakeWords>0&&r.push(l(t.report.newMistakes,b.red(String(e.newMistakeWords))))}return r.push(""),r.push(b.dim.italic(t.report.farewell)),r}function it(e,t,s){if(e.startedAt===null&&e.chaptersCompleted===0)return;let r=ve(e,t,s).join(`
2
2
  `);console.log(Se(r,{title:b.bold.cyan(t.report.title),titleAlignment:"left",borderStyle:"round",borderColor:"gray",padding:{top:0,bottom:0,left:3,right:3},margin:{top:1,bottom:1,left:2,right:0}}))}export{Y as a,Xe as b,nt as c,it as d};
3
- //# sourceMappingURL=chunk-RR77GWZ3.js.map
3
+ //# sourceMappingURL=chunk-G2SXVAHM.js.map
package/dist/cli.js CHANGED
@@ -1,2 +1,2 @@
1
- import{Command as b}from"commander";import{Command as u}from"commander";function r(){let o=new u("config").description("Manage CLI configuration");return o.command("list").description("Show the effective merged config").action(async()=>{let{configList:t}=await import("./config.impl-55HI447G.js");await t()}),o.command("get <key>").description("Get a config value by dotted path (e.g. sounds.keystroke)").action(async t=>{let{configGet:a}=await import("./config.impl-55HI447G.js");await a(t)}),o.command("set <key> <value>").description("Set a config value by dotted path").action(async(t,a)=>{let{configSet:n}=await import("./config.impl-55HI447G.js");await n(t,a)}),o}import{Command as g}from"commander";function e(){let o=new g("dict").description("Manage dictionaries");return o.command("list").description("List dictionaries (\u2713 = locally available)").option("-c, --category <category>","filter by category").option("--local-only","show only locally downloaded dictionaries").action(async t=>{let{dictList:a}=await import("./dict.impl-2SWDIDWC.js");await a(t)}),o.command("search <keyword>").description("Search the upstream registry by name/description/category/tags").option("-c, --category <category>","restrict to category").option("-l, --language <language>","restrict to language").action(async(t,a)=>{let{dictSearch:n}=await import("./dict.impl-2SWDIDWC.js");await n(t,a)}),o.command("pull <id>").description("Download an upstream dictionary into the local cache").action(async t=>{let{dictPull:a}=await import("./dict.impl-2SWDIDWC.js");await a(t)}),o.command("import <file>").description("Import a local qwerty-native JSON dictionary").requiredOption("--id <id>","dictionary id (lowercase, digits, dashes)").action(async(t,a)=>{let{dictImport:n}=await import("./dict.impl-2SWDIDWC.js");await n(t,a)}),o.command("rm <id>").description("Remove a local dictionary").action(async t=>{let{dictRemove:a}=await import("./dict.impl-2SWDIDWC.js");await a(t)}),o}import{Command as y}from"commander";function c(){return new y("doctor").description("Diagnose audio playback and runtime environment").action(async()=>{let{runDoctor:o}=await import("./doctor.impl-KOEIKBKP.js");await o()})}import{Command as w}from"commander";function m(){return new w("practice").argument("[dictId]","dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").option("--stealth","enter stealth mode (minimal UI, no sound)").action(async(o,t)=>{let{runPractice:a}=await import("./practice.impl-2PPCBBK3.js");await a(o,t)})}import{Command as f}from"commander";function d(){return new f("stats").description("Show practice history and trends").option("-d, --days <n>","window size for trend (default 14)","14").option("--top <n>","how many top mistakes to show (default 10)","10").action(async o=>{let{runStats:t}=await import("./stats.impl-WX3BFWI3.js");await t(o)})}import{Command as C}from"commander";function s(){return new C("word").argument("<keyword>").description("Look up a word across local dictionaries").option("--exact","require exact (case-insensitive) match").action(async(o,t)=>{let{runWordLookup:a}=await import("./word.impl-EJNGBD7U.js");await a(o,t)})}import{Command as h}from"commander";function p(){return new h("boss").alias("stealth").description("Start practice in stealth mode (minimal UI, looks like plain terminal output)").argument("[dictId]","dictionary id; falls back to config.defaultDict").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").action(async(o,t)=>{let{runPractice:a}=await import("./practice.impl-2PPCBBK3.js");await a(o,{...t,stealth:!0})})}async function l(){if(!process.stdout.isTTY){console.log("qwerty-cli \u2014 run `qwerty --help` for available commands.");return}let{runMainMenuImpl:o}=await import("./menu.impl-ZUAMXKFD.js");await o()}var i=new b;i.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version("0.0.1-alpha.19");i.addCommand(m());i.addCommand(p());i.addCommand(e());i.addCommand(s());i.addCommand(d());i.addCommand(r());i.addCommand(c());i.action(async()=>{await l()});i.parseAsync(process.argv).catch(o=>{console.error(o instanceof Error?o.message:o),process.exit(1)});
1
+ import{Command as b}from"commander";import{Command as u}from"commander";function r(){let o=new u("config").description("Manage CLI configuration");return o.command("list").description("Show the effective merged config").action(async()=>{let{configList:t}=await import("./config.impl-55HI447G.js");await t()}),o.command("get <key>").description("Get a config value by dotted path (e.g. sounds.keystroke)").action(async t=>{let{configGet:a}=await import("./config.impl-55HI447G.js");await a(t)}),o.command("set <key> <value>").description("Set a config value by dotted path").action(async(t,a)=>{let{configSet:n}=await import("./config.impl-55HI447G.js");await n(t,a)}),o}import{Command as g}from"commander";function e(){let o=new g("dict").description("Manage dictionaries");return o.command("list").description("List dictionaries (\u2713 = locally available)").option("-c, --category <category>","filter by category").option("--local-only","show only locally downloaded dictionaries").action(async t=>{let{dictList:a}=await import("./dict.impl-2SWDIDWC.js");await a(t)}),o.command("search <keyword>").description("Search the upstream registry by name/description/category/tags").option("-c, --category <category>","restrict to category").option("-l, --language <language>","restrict to language").action(async(t,a)=>{let{dictSearch:n}=await import("./dict.impl-2SWDIDWC.js");await n(t,a)}),o.command("pull <id>").description("Download an upstream dictionary into the local cache").action(async t=>{let{dictPull:a}=await import("./dict.impl-2SWDIDWC.js");await a(t)}),o.command("import <file>").description("Import a local qwerty-native JSON dictionary").requiredOption("--id <id>","dictionary id (lowercase, digits, dashes)").action(async(t,a)=>{let{dictImport:n}=await import("./dict.impl-2SWDIDWC.js");await n(t,a)}),o.command("rm <id>").description("Remove a local dictionary").action(async t=>{let{dictRemove:a}=await import("./dict.impl-2SWDIDWC.js");await a(t)}),o}import{Command as y}from"commander";function c(){return new y("doctor").description("Diagnose audio playback and runtime environment").action(async()=>{let{runDoctor:o}=await import("./doctor.impl-KOEIKBKP.js");await o()})}import{Command as w}from"commander";function m(){return new w("practice").argument("[dictId]","dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").option("--stealth","enter stealth mode (minimal UI, no sound)").action(async(o,t)=>{let{runPractice:a}=await import("./practice.impl-FQ4HFJAL.js");await a(o,t)})}import{Command as f}from"commander";function d(){return new f("stats").description("Show practice history and trends").option("-d, --days <n>","window size for trend (default 14)","14").option("--top <n>","how many top mistakes to show (default 10)","10").action(async o=>{let{runStats:t}=await import("./stats.impl-WX3BFWI3.js");await t(o)})}import{Command as C}from"commander";function s(){return new C("word").argument("<keyword>").description("Look up a word across local dictionaries").option("--exact","require exact (case-insensitive) match").action(async(o,t)=>{let{runWordLookup:a}=await import("./word.impl-EJNGBD7U.js");await a(o,t)})}import{Command as h}from"commander";function p(){return new h("boss").alias("stealth").description("Start practice in stealth mode (minimal UI, looks like plain terminal output)").argument("[dictId]","dictionary id; falls back to config.defaultDict").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").action(async(o,t)=>{let{runPractice:a}=await import("./practice.impl-FQ4HFJAL.js");await a(o,{...t,stealth:!0})})}async function l(){if(!process.stdout.isTTY){console.log("qwerty-cli \u2014 run `qwerty --help` for available commands.");return}let{runMainMenuImpl:o}=await import("./menu.impl-A3NQTDOR.js");await o()}var i=new b;i.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version("0.0.1-alpha.20");i.addCommand(m());i.addCommand(p());i.addCommand(e());i.addCommand(s());i.addCommand(d());i.addCommand(r());i.addCommand(c());i.action(async()=>{await l()});i.parseAsync(process.argv).catch(o=>{console.error(o instanceof Error?o.message:o),process.exit(1)});
2
2
  //# sourceMappingURL=cli.js.map
@@ -1,2 +1,2 @@
1
- import{a as o,b as s,c as p,d as c}from"./chunk-RR77GWZ3.js";import{d as n,g as a,i as m}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-MFGIEKBU.js";import{a as i}from"./chunk-VTIB5Q36.js";import"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{render as g}from"ink";import{createElement as h}from"react";async function y(){o();let r=await i();a();let{waitUntilExit:f}=g(h(s,{initial:{name:"main"},initialCfg:r}),{patchConsole:!1,exitOnCtrlC:!1});await f(),p();let t=n();if(t?.type==="stealth"){let{runPractice:u}=await import("./practice.impl-2PPCBBK3.js");await u(t.dictId,{chapter:t.chapterIndex+1,mode:t.mode,stealth:!0});return}let{lang:l,t:d}=e(r.language);c(m(),d,l)}export{y as runMainMenuImpl};
2
- //# sourceMappingURL=menu.impl-ZUAMXKFD.js.map
1
+ import{a as o,b as s,c as p,d as c}from"./chunk-G2SXVAHM.js";import{d as n,g as a,i as m}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-MFGIEKBU.js";import{a as i}from"./chunk-VTIB5Q36.js";import"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{render as g}from"ink";import{createElement as h}from"react";async function y(){o();let r=await i();a();let{waitUntilExit:f}=g(h(s,{initial:{name:"main"},initialCfg:r}),{patchConsole:!1,exitOnCtrlC:!1});await f(),p();let t=n();if(t?.type==="stealth"){let{runPractice:u}=await import("./practice.impl-FQ4HFJAL.js");await u(t.dictId,{chapter:t.chapterIndex+1,mode:t.mode,stealth:!0});return}let{lang:l,t:d}=e(r.language);c(m(),d,l)}export{y as runMainMenuImpl};
2
+ //# sourceMappingURL=menu.impl-A3NQTDOR.js.map
@@ -1,2 +1,2 @@
1
- import{a as c,b as g,c as h,d as x}from"./chunk-RR77GWZ3.js";import{f as m,g as l,i as u}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-MFGIEKBU.js";import{a}from"./chunk-VTIB5Q36.js";import{b as f,e as p}from"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e as d}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import s from"chalk";import{render as v}from"ink";import{createElement as E}from"react";var C=["order","dictation","review","random","loop"];function P(o){return C.includes(o)}async function Y(o,t){if(!process.stdout.isTTY){console.error(s.red("Practice requires an interactive TTY.")),process.exitCode=1;return}let e=await a(),i=o??e.defaultDict;if(!i){console.error(s.red("No dictionary specified. Pass an id or set config.defaultDict.")),process.exitCode=1;return}let n=t.mode??e.defaultMode;if(!P(n)){console.error(s.red(`Invalid mode "${n}". Valid: ${C.join(", ")}`)),process.exitCode=1;return}let M=t.chapter!==void 0?Math.max(0,Number(t.chapter)-1):p(await f(),i),r=t.stealth===!0||e.stealth==="default";r||c(),l();let{waitUntilExit:S}=v(E(g,{initial:{name:"practice",params:{dictId:i,chapterIndex:M,mode:n,stealth:r}},initialCfg:e,inline:r}),{patchConsole:!1,exitOnCtrlC:!1});if(await S(),r||h(),m()){process.stdout.write("\x1B[3F\x1B[0J");return}if(r)return;let{lang:w,t:b}=d(e.language);x(u(),b,w)}export{Y as runPractice};
2
- //# sourceMappingURL=practice.impl-2PPCBBK3.js.map
1
+ import{a as c,b as g,c as h,d as x}from"./chunk-G2SXVAHM.js";import{f as m,g as l,i as u}from"./chunk-6ROGUGNX.js";import"./chunk-BIBS2Q3E.js";import"./chunk-MFGIEKBU.js";import{a}from"./chunk-VTIB5Q36.js";import{b as f,e as p}from"./chunk-GULN5HRV.js";import"./chunk-QG7ZTS2G.js";import{e as d}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import s from"chalk";import{render as v}from"ink";import{createElement as E}from"react";var C=["order","dictation","review","random","loop"];function P(o){return C.includes(o)}async function Y(o,t){if(!process.stdout.isTTY){console.error(s.red("Practice requires an interactive TTY.")),process.exitCode=1;return}let e=await a(),i=o??e.defaultDict;if(!i){console.error(s.red("No dictionary specified. Pass an id or set config.defaultDict.")),process.exitCode=1;return}let n=t.mode??e.defaultMode;if(!P(n)){console.error(s.red(`Invalid mode "${n}". Valid: ${C.join(", ")}`)),process.exitCode=1;return}let M=t.chapter!==void 0?Math.max(0,Number(t.chapter)-1):p(await f(),i),r=t.stealth===!0||e.stealth==="default";r||c(),l();let{waitUntilExit:S}=v(E(g,{initial:{name:"practice",params:{dictId:i,chapterIndex:M,mode:n,stealth:r}},initialCfg:e,inline:r}),{patchConsole:!1,exitOnCtrlC:!1});if(await S(),r||h(),m()){process.stdout.write("\x1B[3F\x1B[0J");return}if(r)return;let{lang:w,t:b}=d(e.language);x(u(),b,w)}export{Y as runPractice};
2
+ //# sourceMappingURL=practice.impl-FQ4HFJAL.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwerty-cli",
3
- "version": "0.0.1-alpha.19",
3
+ "version": "0.0.1-alpha.20",
4
4
  "description": "Terminal clone of qwerty-learner: typing practice for English vocabulary, with chapters, dictation, mistake book, and audio.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,2 +0,0 @@
1
- import{b as gt,e as yt,h as It}from"./chunk-6ROGUGNX.js";import{a as lt,b as dt,c as mt,d as pt,e as ft,f as ht}from"./chunk-BIBS2Q3E.js";import{e as ut}from"./chunk-EBAA2ZKH.js";import{b as O}from"./chunk-MFGIEKBU.js";import"./chunk-VTIB5Q36.js";import{a as V,b as Tt,c as St}from"./chunk-G3DQB7FI.js";import{a as wt}from"./chunk-GULN5HRV.js";import{b as xt,e as _}from"./chunk-QG7ZTS2G.js";import{b as X,d as W,f as c,g as bt}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as H,useEffect as K,useRef as et}from"react";import{Box as p,Text as x,useApp as Me,useInput as j}from"ink";function Ct(t,e=Math.random){let r=[...t];for(let n=r.length-1;n>0;n--){let o=Math.floor(e()*(n+1)),u=r[n];r[n]=r[o],r[o]=u}return r}function Mt(t){let e=t>>>0;return()=>{e=e+1831565813>>>0;let r=Math.imul(e^e>>>15,1|e);return r=r+Math.imul(r^r>>>7,61|r)^r,((r^r>>>14)>>>0)/4294967296}}function kt(t,e){if(e<=0)throw new Error("chapterSize must be positive");let r=[];for(let n=0;n<t.length;n+=e)r.push(t.slice(n,n+e));return r}function Et(t,e,r){if(e==="random"){let n=r===void 0?Math.random:Mt(r);return Ct(t,n)}return t}function G(t){return{target:t,typed:"",errorsThisWord:0}}function Wt(t,e){switch(e.type){case"reset":return{state:{...t,typed:""},effect:"none"};case"backspace":return t.typed.length===0?{state:t,effect:"none"}:{state:{...t,typed:t.typed.slice(0,-1)},effect:"none"};case"char":{let r=t.typed+e.ch,n=[...t.target].slice(0,[...r].length).join("");return r===n?r.length===t.target.length?{state:{...t,typed:r},effect:"correct"}:{state:{...t,typed:r},effect:"progress"}:{state:{...t,typed:"",errorsThisWord:t.errorsThisWord+1},effect:"wrong"}}}}function J(t,e=Date.now()){return t.length===0?{startedAt:e,results:[],current:null,finishedAt:e,playlist:t}:{startedAt:e,results:[],current:{wordIndex:0,wordStartedAt:e,input:G(t[0].name)},finishedAt:null,playlist:t}}function Q(t,e,r=Date.now()){if(!t.current)return{session:t,effect:"none"};let{state:n,effect:o}=Wt(t.current.input,e);if(o==="correct"){let u={word:n.target,errors:n.errorsThisWord,durationMs:r-t.current.wordStartedAt},s=t.current.wordIndex+1,a=[...t.results,u];return s>=t.playlist.length?{session:{...t,results:a,current:null,finishedAt:r},effect:o}:{session:{...t,results:a,current:{wordIndex:s,wordStartedAt:r,input:G(t.playlist[s].name)}},effect:o}}return{session:{...t,current:{...t.current,input:n}},effect:o}}function At(t,e=Date.now()){if(!t.current)return{session:t,effect:"none"};let r={word:t.current.input.target,errors:0,durationMs:e-t.current.wordStartedAt,skipped:!0},n=t.current.wordIndex+1,o=[...t.results,r];return n>=t.playlist.length?{session:{...t,results:o,current:null,finishedAt:e},effect:"skipped"}:{session:{...t,results:o,current:{wordIndex:n,wordStartedAt:e,input:G(t.playlist[n].name)}},effect:"skipped"}}function Z(t){let e=t.results.reduce((o,u)=>o+u.errors,0),r=(t.finishedAt??Date.now())-t.startedAt,n={};for(let o of t.results)o.errors>0&&(n[o.word]=(n[o.word]??0)+o.errors);return{wordCount:t.results.length,errors:e,durationMs:r,perWordErrors:n}}import{useEffect as Bt,useReducer as ce,useRef as se,useState as ae}from"react";import{useInput as ue,useApp as le}from"ink";function de(t,e){if(e.type==="start")return{session:J(e.playlist,e.now),lastEffect:null,effectSeq:0};if(e.type==="skip"){let r=At(t.session,e.now);return{session:r.session,lastEffect:r.effect,effectSeq:t.effectSeq+1}}if(e.type==="event"){if(e.key.backspace||e.key.delete){let o=Q(t.session,{type:"backspace"},e.now);return{session:o.session,lastEffect:o.effect,effectSeq:t.effectSeq+1}}if(e.input.length===0)return t;let r=t.session,n=t.lastEffect;for(let o of e.input){let u=Q(r,{type:"char",ch:o},e.now);if(r=u.session,n=u.effect,r.finishedAt!==null)break}return{session:r,lastEffect:n,effectSeq:t.effectSeq+1}}return t}function me(t){let e=[...t];if(e.some(n=>n.codePointAt(0)>=128))return{kind:"ime",cleaned:""};let r=e.filter(n=>{let o=n.codePointAt(0);return o>=32&&o<=126}).join("");return{kind:r.length>0?"valid":"noise",cleaned:r}}function Pt({playlist:t,onComplete:e,onTab:r,onEscape:n,onSkip:o,onImeBlock:u,onValidInput:s,enabled:a=!0}){let[l,b]=ce(de,void 0,()=>({session:J(t,Date.now()),lastEffect:null,effectSeq:0})),I=se(!1),[T,C]=ae(0),{exit:w}=le();return ue((f,g)=>{if(g.ctrl&&f==="c"){w();return}if(g.ctrl&&f==="n"){o?.(),b({type:"skip",now:Date.now()});return}if(g.escape){n?.();return}if(g.tab){r?.();return}if(g.upArrow||g.downArrow||g.leftArrow||g.rightArrow||g.return||g.ctrl||g.meta)return;let{kind:M,cleaned:B}=me(f);if(M==="ime"){u?.();return}M!=="noise"&&(s?.(),b({type:"event",input:B,key:g,now:Date.now()}))},{isActive:a}),Bt(()=>{l.session.finishedAt!==null&&!I.current&&(I.current=!0,e(l.session))},[l.session,e]),Bt(()=>{if(l.session.finishedAt!==null)return;let f=setInterval(()=>C(g=>g+1),1e3);return()=>clearInterval(f)},[l.session.finishedAt]),{session:l.session,lastEffect:l.lastEffect,effectSeq:l.effectSeq,tick:T}}import{useEffect as pe,useRef as fe}from"react";function vt(t){let e=fe(!1);return pe(()=>{e.current||(e.current=!0,lt(!t.enabled).catch(()=>{}))},[t.enabled]),{keystroke:()=>t.enabled&&dt(),correct:()=>t.enabled&&mt(),wrong:()=>t.enabled&&pt(),pronounce:r=>{t.enabled&&t.autoplayPronunciation&&ft(r,t.accent,t.pronunciationRate,t.pronunciationSource)},prefetch:r=>{t.enabled&&ht(r,t.accent,t.pronunciationSource)}}}import{useCallback as he}from"react";function $t(t){return he(async e=>{let r={ts:new Date().toISOString(),dictId:t.dictId,chapter:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors};await wt(r),It({dictId:t.dictId,chapterIndex:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors});let n=Object.entries(e.perWordErrors).filter(([,u])=>u>0);if(n.length===0)return;let o=await V();for(let[u,s]of n)o=St(o,u,t.dictId,s);await Tt(o)},[t.dictId,t.chapterIndex,t.mode])}function Rt(t=process.env){return t.KITTY_WINDOW_ID?{supportsDoubleHeight:!1}:t.VTE_VERSION?{supportsDoubleHeight:!1}:t.KONSOLE_VERSION?{supportsDoubleHeight:!1}:t.WT_SESSION?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="Apple_Terminal"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="iTerm.app"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="WezTerm"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="mintty"?{supportsDoubleHeight:!0}:t.ALACRITTY_SOCKET||t.ALACRITTY_LOG?{supportsDoubleHeight:!0}:t.XTERM_VERSION?{supportsDoubleHeight:!0}:{supportsDoubleHeight:!1}}import{Box as ge,Text as Dt,useStdout as xe}from"ink";import{jsx as Lt,jsxs as we}from"react/jsx-runtime";var Nt=new Map;function be(t){let e=Nt.get(t);if(e)return e;let r=parseInt(t.slice(1,3),16),n=parseInt(t.slice(3,5),16),o=parseInt(t.slice(5,7),16),u=`\x1B[1;38;2;${r};${n};${o}m`;return Nt.set(t,u),u}var ye="\x1B[0m";function _t({target:t,typed:e,error:r=!1,hideTarget:n=!1}){let{stdout:o}=xe(),u=o?.columns??80,s=[...t],a=[...e],l=s.length*2,b=Math.max(0,Math.floor((u-l)/2)),I=" ".repeat(Math.floor(b/2)),T=s.map((f,g)=>{let M=g<a.length,B=n&&!M?"_":M?a[g]:f,v=r?c.error:M?c.accent:c.muted;return`${be(v)}${B}${ye}`}).join(""),C=`\x1B#3${I}${T}`,w=`\x1B#4${I}${T}`;return we(ge,{flexDirection:"column",paddingY:3,width:"100%",children:[Lt(Dt,{children:C}),Lt(Dt,{children:w})]})}import{jsx as Ht}from"react/jsx-runtime";var tt=null;function Te(){return tt||(tt=Rt()),tt}function Ot(t){let{cfg:e}=O(),r=process.stdout?.columns??80,n=Te(),o=e.wordDisplay==="huge"||e.wordDisplay==="auto"&&n.supportsDoubleHeight,u=r>=2*[...t.target].length+4;return o&&u?Ht(_t,{...t}):Ht(bt,{...t})}import{Box as N,Text as m,useStdout as Se}from"ink";import{Fragment as Ce,jsx as d,jsxs as P}from"react/jsx-runtime";var jt=28;function qt(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function Ie(){let{stdout:t}=Se(),e=t?.columns??80;return Math.max(20,e-jt)}function A({left:t,right:e}){let r=Ie();return P(N,{children:[d(N,{width:r,children:t}),d(N,{width:jt,justifyContent:"flex-end",children:e})]})}function Vt(t){let e=W(),r=[...t.target],n=[...t.typed],o=P(N,{children:[r.map((C,w)=>{let f=w<n.length,g=t.hideTarget&&!f?"_":f?n[w]:C,M=t.error?c.error:f?c.accent:c.muted;return d(m,{bold:!0,color:M,children:g},w)}),t.phonetic&&P(Ce,{children:[d(m,{children:" "}),d(m,{italic:!0,dimColor:!0,color:c.muted,children:t.phonetic})]})]}),u=t.translation.length>0?d(m,{color:c.primary,children:t.translation[0]}):d(m,{children:" "}),s=t.info,a=Number.isInteger(s.accPct)?`${s.accPct}`:s.accPct.toFixed(1),l=!t.imeBlocked&&t.audioWarning!==null,b=t.imeBlocked?d(m,{color:c.warning,children:e.practice.imeWarningShort}):l?d(m,{color:c.warning,children:e.practice.audioWarningShort}):s.visible?d(m,{color:c.muted,children:`${s.dictName} \xB7 ${s.chapterLabel}`}):d(m,{children:" "}),I=t.imeBlocked||l?d(m,{children:" "}):s.visible?d(m,{color:c.muted,children:`${s.completed}/${s.total} \xB7 ${s.wpm}wpm \xB7 ${a}%`}):d(m,{children:" "}),T=t.imeBlocked||l?d(m,{children:" "}):s.visible?d(m,{color:c.muted,children:qt(s.elapsedMs)}):d(m,{children:" "});return P(N,{flexDirection:"column",children:[d(A,{left:o,right:b}),d(A,{left:d(m,{children:" "}),right:I}),d(A,{left:u,right:T})]})}function Gt(){let t=W();return P(N,{flexDirection:"column",children:[d(A,{left:d(m,{color:c.warning,children:t.stealth.paused}),right:d(m,{color:c.muted,children:t.stealth.pausedHintRight})}),d(A,{left:d(m,{children:" "}),right:P(m,{color:c.muted,children:["Esc ",t.common.back]})}),d(A,{left:d(m,{children:" "}),right:d(m,{children:" "})})]})}function Ft(t){let e=W(),r=Number.isInteger(t.accPct)?`${t.accPct}`:t.accPct.toFixed(1),n=`${e.stealth.chapterDone} \xB7 ${t.wordCount}w \xB7 ${t.wpm}wpm \xB7 ${r}% \xB7 ${qt(t.durationMs)}`;return P(N,{flexDirection:"column",children:[d(A,{left:d(m,{color:c.success,children:n}),right:d(m,{color:c.muted,children:e.stealth.nextHintRight})}),d(A,{left:d(m,{children:" "}),right:P(m,{color:c.muted,children:["Esc ",e.common.back]})}),d(A,{left:d(m,{children:" "}),right:d(m,{children:" "})})]})}import{jsx as i,jsxs as E}from"react/jsx-runtime";function $r({params:t}){let{dictId:e,chapterIndex:r,mode:n}=t,{cfg:o}=O(),u=W(),[s,a]=H("loading"),[l,b]=H(null),[I,T]=H(null);return K(()=>{let C=!1;return a("loading"),b(null),T(null),(async()=>{try{let w=await ut(e);if(C)return;if(n==="review"){let B=await V();if(C)return;let v=w.filter(q=>B[q.name]?.count).slice(0,o.chapterSize);if(v.length===0){T(u.practice.errors.noMistakes),a("error");return}b({playlist:v,totalChapters:1}),a("typing");return}let f=kt(w,o.chapterSize);if(f.length===0){T(u.practice.errors.dictEmpty(e)),a("error");return}let g=Math.max(0,Math.min(f.length-1,r)),M=Et(f[g],n);b({playlist:M,totalChapters:f.length}),a("typing")}catch(w){if(C)return;T(w.message),a("error")}})(),()=>{C=!0}},[e,r,n,o.chapterSize,u]),s==="loading"?i(ve,{text:u.practice.loading,color:c.muted}):s==="error"?i(Be,{msg:I??u.practice.errors.unknown}):l?i(ke,{params:t,loaded:l,phase:s,setPhase:a},`${e}-${r}-${n}-${t.stealth?"s":"n"}`):null}function ke({params:t,loaded:e,phase:r,setPhase:n}){let{dictId:o,chapterIndex:u,mode:s}=t,a=t.stealth===!0,{cfg:l}=O(),b=X(),{exit:I}=Me(),T=()=>b.stack.length>1?b.back():I(),C=$t({dictId:o,chapterIndex:u,mode:s}),w=xt(o),f=vt({enabled:!a&&l.sounds.master,accent:l.accent,autoplayPronunciation:!a&&l.autoplayPronunciation,pronunciationRate:l.sounds.pronunciationRate,pronunciationSource:l.sounds.pronunciationSource}),g=gt(),M=l.sounds.master?g.warning:null,B=et(!1),v=et(0),q=et(-1),[zt,nt]=H(!1),[ot,Ut]=H(null),[it,ct]=H(!1);K(()=>{if(ot===null)return;let h=setTimeout(()=>nt(!1),2e3);return()=>clearTimeout(h)},[ot]);let{session:S,lastEffect:$,effectSeq:Y,tick:Xt}=Pt({playlist:e.playlist,enabled:r==="typing",onComplete:h=>{B.current||(B.current=!0,n("summary"),Promise.resolve(C(Z(h))).catch(y=>{console.error("Failed to persist session:",y)}))},onEscape:()=>n(r==="paused"?"typing":"paused"),onTab:a?void 0:()=>{let h=S.current?e.playlist[S.current.wordIndex]:void 0;h&&f.pronounce(h.name)},onImeBlock:()=>ct(!0),onValidInput:()=>ct(!1)});K(()=>{a||Y!==v.current&&(v.current=Y,$!==null&&($==="wrong"&&l.sounds.feedback&&f.wrong(),$==="progress"&&l.sounds.keystroke&&f.keystroke(),$==="correct"&&(l.sounds.feedback&&f.correct(),l.sounds.keystroke&&f.keystroke())))},[a,Y,$,f,l.sounds.feedback,l.sounds.keystroke]),K(()=>{if(a)return;let h=S.current?.wordIndex??-1;if(h===-1||h===q.current)return;q.current=h;let y=e.playlist[h],D=e.playlist[h+1];y&&l.autoplayPronunciation&&f.pronounce(y.name),D&&f.prefetch(D.name)},[a,S.current?.wordIndex,f,l.autoplayPronunciation,e.playlist]),j((h,y)=>{if(y.tab){nt(!0),Ut(Date.now());return}},{isActive:a&&r==="typing"}),j((h,y)=>{if(y.return){n("typing");return}if(y.escape){T();return}},{isActive:r==="paused"}),j((h,y)=>{y.ctrl&&h==="c"&&(yt(!0),I())},{isActive:a&&r==="paused"}),j((h,y)=>{if(y.escape){T();return}if(y.return){let D=u+1;s==="loop"?b.replace({name:"practice",params:{dictId:o,chapterIndex:u,mode:s,stealth:t.stealth}}):s==="review"||D>=e.totalChapters?T():b.replace({name:"practice",params:{dictId:o,chapterIndex:D,mode:s,stealth:t.stealth}});return}if(h==="m"){b.replace({name:"practice",params:{dictId:o,chapterIndex:0,mode:"review",stealth:t.stealth}});return}},{isActive:r==="summary"});let R=S.results.length,Jt=S.results.reduce((h,y)=>h+y.errors,0),z=Date.now()-S.startedAt,st=z/6e4,at=st>0?Math.round(R/st*10)/10:0,k=r==="summary"?Z(S):null;if(a){if(r==="paused")return i(Gt,{});if(r==="summary"&&k){let L=k.durationMs/6e4,re=L>0?Math.round(k.wordCount/L*10)/10:0,ne=Object.keys(k.perWordErrors).length,oe=k.wordCount===0?1:Math.max(0,(k.wordCount-ne)/k.wordCount),ie=Math.round(oe*1e3)/10;return i(Ft,{wordCount:k.wordCount,errors:k.errors,durationMs:k.durationMs,wpm:re,accPct:ie})}let h=S.current?e.playlist[S.current.wordIndex]:e.playlist[e.playlist.length-1],y=S.current?.input??{target:"",typed:"",errorsThisWord:0},D=new Set(S.results.filter(L=>L.errors>0).map(L=>L.word)).size,Zt=R===0?1:Math.max(0,(R-D)/R),te=Math.round(Zt*1e3)/10,ee=s==="review"?"review":`ch ${u+1}/${e.totalChapters}`;return i(Vt,{target:h?.name??"",typed:y.typed,hideTarget:s==="dictation",phonetic:Kt(h,l.accent),translation:h?.trans??[],error:$==="wrong",imeBlocked:it,audioWarning:M,info:{visible:zt,dictName:_(w,24),chapterLabel:ee,completed:R,total:e.playlist.length,wpm:at,accPct:te,elapsedMs:z}})}if(r==="paused")return i(Ae,{dictName:w,chapterIndex:u,totalChapters:e.totalChapters,mode:s,completed:R,total:e.playlist.length});if(r==="summary"&&k)return i($e,{dictName:w,chapterIndex:u,totalChapters:e.totalChapters,mode:s,summary:k});let U=S.current?e.playlist[S.current.wordIndex]:e.playlist[e.playlist.length-1],Qt=S.current?.input??{target:"",typed:"",errorsThisWord:0};return i(Ee,{dictName:w,chapterIndex:u,totalChapters:e.totalChapters,mode:s,accent:l.accent,completed:R,total:e.playlist.length,errors:Jt,wpm:at,elapsedMs:z,target:U?.name??"",typed:Qt.typed,flashError:$==="wrong",hideTarget:s==="dictation",phonetic:Kt(U,l.accent),translation:U?.trans??[],imeBlocked:it,audioWarning:M})}function Kt(t,e){return t?(e==="us"?t.usphone:t.ukphone)??null:null}function rt(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function Ee(t){let e=W(),r=t.total===0?0:t.completed/t.total;return E(p,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[i(We,{dictName:t.dictName,chapterIndex:t.chapterIndex,totalChapters:t.totalChapters,mode:t.mode,accent:t.accent,completed:t.completed,total:t.total,elapsedMs:t.elapsedMs}),E(p,{flexGrow:1,flexDirection:"column",alignItems:"center",justifyContent:"center",children:[i(Ot,{target:t.target,typed:t.typed,error:t.flashError,hideTarget:t.hideTarget}),t.phonetic&&i(p,{marginTop:1,children:i(x,{italic:!0,dimColor:!0,color:c.muted,children:t.phonetic})}),t.translation.length>0&&i(p,{marginTop:1,flexDirection:"column",alignItems:"center",children:t.translation.slice(0,2).map((n,o)=>i(x,{color:c.primary,children:n},o))}),t.imeBlocked&&i(p,{marginTop:1,children:i(x,{color:c.warning,children:e.practice.imeWarning})}),!t.imeBlocked&&t.audioWarning&&i(p,{marginTop:1,children:i(x,{color:c.warning,children:t.audioWarning})})]}),E(p,{flexDirection:"column",children:[i(Yt,{frac:r}),i(p,{justifyContent:"center",marginTop:1,children:E(x,{color:c.muted,children:[t.completed,"/",t.total," \xB7 ",rt(t.elapsedMs)," \xB7 ",t.wpm," ",e.practice.statCards.wpm," \xB7 ",t.errors," ",e.practice.statCards.errors]})}),i(p,{justifyContent:"center",marginTop:1,children:i(x,{color:c.muted,children:e.practice.footers.typing})})]})]})}function We(t){let e=W(),r=e.practice.modes[t.mode],n=e.practice.accents[t.accent],o=_(t.dictName,20),u=t.mode==="review"?`${o} \xB7 ${e.practice.reviewLabel} \xB7 ${n}`:`${o} \xB7 ${e.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${r} \xB7 ${n}`,s=`${t.completed}/${t.total} \xB7 ${rt(t.elapsedMs)}`;return E(p,{children:[i(x,{color:c.muted,children:u}),i(p,{flexGrow:1}),i(x,{color:c.muted,children:s})]})}function Yt({frac:t}){let e=process.stdout.columns??80,r=Math.max(20,Math.min(72,e-16)),n=Math.round(r*Math.max(0,Math.min(1,t))),o=r-n;return E(p,{justifyContent:"center",children:[i(x,{color:c.accent,children:"\u2501".repeat(n)}),i(x,{color:c.muted,children:"\u2500".repeat(o)})]})}function Ae(t){let e=W(),r=t.total===0?0:t.completed/t.total,n=t.mode==="review"?`${_(t.dictName,20)} \xB7 ${e.practice.reviewLabel}`:`${_(t.dictName,20)} \xB7 ${e.practice.pause.chapter(t.chapterIndex+1,t.totalChapters)}`;return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{bold:!0,color:c.warning,children:e.practice.pause.title}),i(p,{marginTop:1,children:i(x,{color:c.muted,children:n})}),i(p,{marginTop:2,children:i(Yt,{frac:r})}),i(p,{marginTop:1,children:i(x,{color:c.muted,children:e.practice.pause.progress(t.completed,t.total)})}),i(p,{marginTop:2,children:i(x,{color:c.muted,children:e.practice.pause.hint})})]})}function Be({msg:t}){let e=W();return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{color:c.error,children:t}),i(p,{marginTop:2,children:E(x,{color:c.muted,children:["Esc ",e.common.back]})}),i(Pe,{})]})}function Pe(){let t=X();return j((e,r)=>{r.escape&&t.back()}),null}function ve({text:t,color:e}){return i(p,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(x,{color:e,children:t})})}function $e(t){let{summary:e}=t,r=e.durationMs/6e4,n=r>0?Math.round(e.wordCount/r*10)/10:0,o=Object.keys(e.perWordErrors).length,u=e.wordCount===0?1:Math.max(0,(e.wordCount-o)/e.wordCount),s=Math.round(u*1e3)/10,a=W(),l=a.practice.modes[t.mode],b=_(t.dictName,20),I=t.mode==="review"?`${b} \xB7 ${a.practice.reviewLabel}`:`${b} \xB7 ${a.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${l}`,C=`Enter ${t.mode==="loop"?a.practice.summary.loopAgain:t.mode==="review"||t.chapterIndex+1>=t.totalChapters?a.practice.summary.backMenu:a.practice.summary.nextChapter} \xB7 m ${a.practice.summary.reviewMistakes} \xB7 Esc ${a.practice.summary.backMenu}`;return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",paddingY:1,width:"100%",height:"100%",children:[i(x,{bold:!0,color:c.success,children:a.practice.chapterComplete}),i(p,{marginTop:1,children:i(x,{color:c.muted,children:I})}),E(p,{marginTop:3,flexDirection:"row",justifyContent:"center",children:[i(F,{label:a.practice.statCards.words,value:String(e.wordCount),color:c.text}),i(F,{label:a.practice.statCards.errors,value:String(e.errors),color:e.errors>0?c.error:c.muted}),i(F,{label:a.practice.statCards.wpm,value:String(n),color:c.accent}),i(F,{label:a.practice.statCards.accuracy,value:`${s}%`,color:c.accent})]}),i(p,{marginTop:2,children:i(x,{color:c.muted,children:a.practice.statCards.elapsed(rt(e.durationMs))})}),i(p,{flexGrow:1}),i(p,{marginTop:2,children:i(x,{color:c.muted,children:C})})]})}function F({label:t,value:e,color:r}){return E(p,{flexDirection:"column",alignItems:"center",marginX:3,children:[i(x,{bold:!0,color:r,children:e}),i(x,{color:c.muted,children:t})]})}export{$r as PracticeScreen};
2
- //# sourceMappingURL=PracticeScreen-PXPBME6H.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ui/screens/PracticeScreen.tsx","../src/util/shuffle.ts","../src/domain/chapters.ts","../src/domain/input-buffer.ts","../src/domain/session.ts","../src/ui/hooks/useWordLoop.ts","../src/ui/hooks/useAudio.ts","../src/ui/hooks/useSessionPersistence.ts","../src/util/term-caps.ts","../src/ui/components/BigWordHuge.tsx","../src/ui/components/BigWordAuto.tsx","../src/ui/screens/StealthPracticeLayout.tsx"],"sourcesContent":["import { useState, useEffect, useRef } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport type { Mode } from '../../domain/chapters.js';\nimport { chunkChapters, buildPlaylist } from '../../domain/chapters.js';\nimport { sessionSummary } from '../../domain/session.js';\nimport { loadMistakes } from '../../domain/mistakes.js';\nimport { ensureDictionary } from '../../infra/dict-downloader.js';\nimport { useWordLoop } from '../hooks/useWordLoop.js';\nimport { useAudio } from '../hooks/useAudio.js';\nimport { useSessionPersistence } from '../hooks/useSessionPersistence.js';\nimport { useNav, type PracticeParams } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { BigWordAuto } from '../components/BigWordAuto.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { StealthTyping, StealthPaused, StealthSummary } from './StealthPracticeLayout.js';\nimport { setSilentExit } from '../../util/post-exit-action.js';\n\ntype Phase = 'loading' | 'typing' | 'paused' | 'summary' | 'error';\n\ntype Loaded = {\n playlist: Word[];\n totalChapters: number;\n};\n\nexport function PracticeScreen({ params }: { params: PracticeParams }) {\n const { dictId, chapterIndex, mode } = params;\n const { cfg } = useAppState();\n const t = useStrings();\n\n const [phase, setPhase] = useState<Phase>('loading');\n const [loaded, setLoaded] = useState<Loaded | null>(null);\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n setPhase('loading');\n setLoaded(null);\n setErrorMsg(null);\n (async () => {\n try {\n const words = await ensureDictionary(dictId);\n if (cancelled) return;\n if (mode === 'review') {\n const book = await loadMistakes();\n if (cancelled) return;\n const reviewWords = words.filter((w) => book[w.name]?.count).slice(0, cfg.chapterSize);\n if (reviewWords.length === 0) {\n setErrorMsg(t.practice.errors.noMistakes);\n setPhase('error');\n return;\n }\n setLoaded({ playlist: reviewWords, totalChapters: 1 });\n setPhase('typing');\n return;\n }\n const chapters = chunkChapters(words, cfg.chapterSize);\n if (chapters.length === 0) {\n setErrorMsg(t.practice.errors.dictEmpty(dictId));\n setPhase('error');\n return;\n }\n const idx = Math.max(0, Math.min(chapters.length - 1, chapterIndex));\n const playlist = buildPlaylist(chapters[idx]!, mode);\n setLoaded({ playlist, totalChapters: chapters.length });\n setPhase('typing');\n } catch (err) {\n if (cancelled) return;\n setErrorMsg((err as Error).message);\n setPhase('error');\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [dictId, chapterIndex, mode, cfg.chapterSize, t]);\n\n if (phase === 'loading') {\n return <Centered text={t.practice.loading} color={PALETTE.muted} />;\n }\n if (phase === 'error') {\n return <ErrorView msg={errorMsg ?? t.practice.errors.unknown} />;\n }\n if (!loaded) return null;\n\n return (\n <PracticeRunner\n key={`${dictId}-${chapterIndex}-${mode}-${params.stealth ? 's' : 'n'}`}\n params={params}\n loaded={loaded}\n phase={phase}\n setPhase={setPhase}\n />\n );\n}\n\nfunction PracticeRunner({\n params,\n loaded,\n phase,\n setPhase,\n}: {\n params: PracticeParams;\n loaded: Loaded;\n phase: Phase;\n setPhase: (p: Phase) => void;\n}) {\n const { dictId, chapterIndex, mode } = params;\n const stealth = params.stealth === true;\n const { cfg } = useAppState();\n const nav = useNav();\n const { exit } = useApp();\n const goBack = () => (nav.stack.length > 1 ? nav.back() : exit());\n const persist = useSessionPersistence({ dictId, chapterIndex, mode });\n const dictName = useDictName(dictId);\n\n const audio = useAudio({\n enabled: !stealth && cfg.sounds.master,\n accent: cfg.accent,\n autoplayPronunciation: !stealth && cfg.autoplayPronunciation,\n pronunciationRate: cfg.sounds.pronunciationRate,\n pronunciationSource: cfg.sounds.pronunciationSource,\n });\n const audioStatus = useAudioStatus();\n // Only surface the warning when the user opted in to sounds — if they\n // disabled sounds via config, \"no player found\" isn't a problem to flag.\n const audioWarn = cfg.sounds.master ? audioStatus.warning : null;\n\n const finishedRef = useRef(false);\n const lastEffectSeqRef = useRef<number>(0);\n const lastIndexRef = useRef<number>(-1);\n const [infoVisible, setInfoVisible] = useState(false);\n const [infoShownAt, setInfoShownAt] = useState<number | null>(null);\n const [imeBlocked, setImeBlocked] = useState(false);\n\n useEffect(() => {\n if (infoShownAt === null) return;\n const id = setTimeout(() => setInfoVisible(false), 2000);\n return () => clearTimeout(id);\n }, [infoShownAt]);\n\n const { session, lastEffect, effectSeq, tick } = useWordLoop({\n playlist: loaded.playlist,\n enabled: phase === 'typing',\n onComplete: (s) => {\n if (finishedRef.current) return;\n finishedRef.current = true;\n setPhase('summary');\n Promise.resolve(persist(sessionSummary(s))).catch((err) => {\n console.error('Failed to persist session:', err);\n });\n },\n onEscape: () => setPhase(phase === 'paused' ? 'typing' : 'paused'),\n onTab: stealth\n ? undefined\n : () => {\n const cur = session.current ? loaded.playlist[session.current.wordIndex] : undefined;\n if (cur) void audio.pronounce(cur.name);\n },\n onImeBlock: () => setImeBlocked(true),\n onValidInput: () => setImeBlocked(false),\n });\n\n useEffect(() => {\n if (stealth) return;\n if (effectSeq === lastEffectSeqRef.current) return;\n lastEffectSeqRef.current = effectSeq;\n if (lastEffect === null) return;\n if (lastEffect === 'wrong' && cfg.sounds.feedback) audio.wrong();\n if (lastEffect === 'progress' && cfg.sounds.keystroke) audio.keystroke();\n if (lastEffect === 'correct') {\n if (cfg.sounds.feedback) audio.correct();\n if (cfg.sounds.keystroke) audio.keystroke();\n }\n }, [stealth, effectSeq, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);\n\n useEffect(() => {\n if (stealth) return;\n const idx = session.current?.wordIndex ?? -1;\n if (idx === -1) return;\n if (idx === lastIndexRef.current) return;\n lastIndexRef.current = idx;\n const cur = loaded.playlist[idx];\n const next = loaded.playlist[idx + 1];\n if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);\n if (next) audio.prefetch(next.name);\n }, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);\n\n void tick;\n\n useInput(\n (_input, key) => {\n // Node's readline normalizes byte 0x09 (Ctrl+I) to {name:'tab', ctrl:false},\n // so key.ctrl && input === 'i' would never match. Tab and Ctrl+I both arrive\n // here as key.tab — bind to that. In stealth mode Tab has no other use\n // (onTab is disabled below), so this is non-conflicting.\n if (key.tab) {\n setInfoVisible(true);\n setInfoShownAt(Date.now());\n return;\n }\n },\n { isActive: stealth && phase === 'typing' },\n );\n\n useInput(\n (_input, key) => {\n if (key.return) {\n setPhase('typing');\n return;\n }\n if (key.escape) {\n goBack();\n return;\n }\n },\n { isActive: phase === 'paused' },\n );\n\n // Stealth + paused only: Ctrl+C exits silently — erase the 3 inline rows\n // (handled in practice.impl.ts after waitUntilExit) and skip the session\n // report. Normal Ctrl+C from typing keeps the 3 rows in scrollback.\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n setSilentExit(true);\n exit();\n }\n },\n { isActive: stealth && phase === 'paused' },\n );\n\n useInput(\n (input, key) => {\n if (key.escape) {\n goBack();\n return;\n }\n if (key.return) {\n const nextIdx = chapterIndex + 1;\n if (mode === 'loop') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex, mode, stealth: params.stealth },\n });\n } else if (mode === 'review' || nextIdx >= loaded.totalChapters) {\n goBack();\n } else {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth },\n });\n }\n return;\n }\n if (input === 'm') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: 0, mode: 'review', stealth: params.stealth },\n });\n return;\n }\n },\n { isActive: phase === 'summary' },\n );\n\n const completed = session.results.length;\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const elapsedMs = Date.now() - session.startedAt;\n const minutes = elapsedMs / 60000;\n const wpm = minutes > 0 ? Math.round((completed / minutes) * 10) / 10 : 0;\n\n const summary = phase === 'summary' ? sessionSummary(session) : null;\n\n if (stealth) {\n if (phase === 'paused') return <StealthPaused />;\n if (phase === 'summary' && summary) {\n const sMinutes = summary.durationMs / 60000;\n const sWpm = sMinutes > 0 ? Math.round((summary.wordCount / sMinutes) * 10) / 10 : 0;\n const sErrWords = Object.keys(summary.perWordErrors).length;\n const sAcc =\n summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - sErrWords) / summary.wordCount);\n const sAccPct = Math.round(sAcc * 1000) / 10;\n return (\n <StealthSummary\n wordCount={summary.wordCount}\n errors={summary.errors}\n durationMs={summary.durationMs}\n wpm={sWpm}\n accPct={sAccPct}\n />\n );\n }\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n const errWords = new Set(\n session.results.filter((r) => r.errors > 0).map((r) => r.word),\n ).size;\n const accFrac =\n completed === 0 ? 1 : Math.max(0, (completed - errWords) / completed);\n const accPct = Math.round(accFrac * 1000) / 10;\n const chapterLabel =\n mode === 'review'\n ? 'review'\n : `ch ${chapterIndex + 1}/${loaded.totalChapters}`;\n return (\n <StealthTyping\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n error={lastEffect === 'wrong'}\n imeBlocked={imeBlocked}\n audioWarning={audioWarn}\n info={{\n visible: infoVisible,\n dictName: truncateName(dictName, 24),\n chapterLabel,\n completed,\n total: loaded.playlist.length,\n wpm,\n accPct,\n elapsedMs,\n }}\n />\n );\n }\n\n if (phase === 'paused') {\n return (\n <PausedView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n completed={completed}\n total={loaded.playlist.length}\n />\n );\n }\n\n if (phase === 'summary' && summary) {\n return (\n <SummaryView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n summary={summary}\n />\n );\n }\n\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n\n return (\n <TypingLayout\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n accent={cfg.accent}\n completed={completed}\n total={loaded.playlist.length}\n errors={errors}\n wpm={wpm}\n elapsedMs={elapsedMs}\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n flashError={lastEffect === 'wrong'}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n imeBlocked={imeBlocked}\n audioWarning={audioWarn}\n />\n );\n}\n\nfunction pickPhonetic(word: Word | undefined, accent: 'us' | 'uk'): string | null {\n if (!word) return null;\n const p = accent === 'us' ? word.usphone : word.ukphone;\n return p ?? null;\n}\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction TypingLayout(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n errors: number;\n wpm: number;\n elapsedMs: number;\n target: string;\n typed: string;\n flashError: boolean;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n imeBlocked: boolean;\n audioWarning: string | null;\n}) {\n const t = useStrings();\n const progressFrac = props.total === 0 ? 0 : props.completed / props.total;\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <StatusBar\n dictName={props.dictName}\n chapterIndex={props.chapterIndex}\n totalChapters={props.totalChapters}\n mode={props.mode}\n accent={props.accent}\n completed={props.completed}\n total={props.total}\n elapsedMs={props.elapsedMs}\n />\n\n <Box flexGrow={1} flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\">\n <BigWordAuto\n target={props.target}\n typed={props.typed}\n error={props.flashError}\n hideTarget={props.hideTarget}\n />\n\n {props.phonetic && (\n <Box marginTop={1}>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </Box>\n )}\n\n {props.translation.length > 0 && (\n <Box marginTop={1} flexDirection=\"column\" alignItems=\"center\">\n {props.translation.slice(0, 2).map((tr, i) => (\n <Text key={i} color={PALETTE.primary}>\n {tr}\n </Text>\n ))}\n </Box>\n )}\n\n {props.imeBlocked && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.practice.imeWarning}</Text>\n </Box>\n )}\n\n {!props.imeBlocked && props.audioWarning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{props.audioWarning}</Text>\n </Box>\n )}\n </Box>\n\n <Box flexDirection=\"column\">\n <ProgressBar frac={progressFrac} />\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>\n {props.completed}/{props.total} · {fmtTime(props.elapsedMs)} · {props.wpm} {t.practice.statCards.wpm} · {props.errors} {t.practice.statCards.errors}\n </Text>\n </Box>\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.footers.typing}</Text>\n </Box>\n </Box>\n </Box>\n );\n}\n\nfunction StatusBar(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n elapsedMs: number;\n}) {\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const accentName = t.practice.accents[props.accent];\n const name = truncateName(props.dictName, 20);\n const left =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel} · ${accentName}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName} · ${accentName}`;\n const right = `${props.completed}/${props.total} · ${fmtTime(props.elapsedMs)}`;\n return (\n <Box>\n <Text color={PALETTE.muted}>{left}</Text>\n <Box flexGrow={1} />\n <Text color={PALETTE.muted}>{right}</Text>\n </Box>\n );\n}\n\nfunction ProgressBar({ frac }: { frac: number }) {\n const cols = process.stdout.columns ?? 80;\n const width = Math.max(20, Math.min(72, cols - 16));\n const filled = Math.round(width * Math.max(0, Math.min(1, frac)));\n const empty = width - filled;\n return (\n <Box justifyContent=\"center\">\n <Text color={PALETTE.accent}>{'━'.repeat(filled)}</Text>\n <Text color={PALETTE.muted}>{'─'.repeat(empty)}</Text>\n </Box>\n );\n}\n\nfunction PausedView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n completed: number;\n total: number;\n}) {\n const t = useStrings();\n const frac = props.total === 0 ? 0 : props.completed / props.total;\n const subtitle =\n props.mode === 'review'\n ? `${truncateName(props.dictName, 20)} · ${t.practice.reviewLabel}`\n : `${truncateName(props.dictName, 20)} · ${t.practice.pause.chapter(props.chapterIndex + 1, props.totalChapters)}`;\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.warning}>\n {t.practice.pause.title}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n <Box marginTop={2}>\n <ProgressBar frac={frac} />\n </Box>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.pause.progress(props.completed, props.total)}</Text>\n </Box>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.pause.hint}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction ErrorView({ msg }: { msg: string }) {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.error}>{msg}</Text>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>Esc {t.common.back}</Text>\n </Box>\n <BackKey />\n </Box>\n );\n}\n\nfunction BackKey() {\n const nav = useNav();\n useInput((_input, key) => {\n if (key.escape) nav.back();\n });\n return null;\n}\n\nfunction Centered({ text, color }: { text: string; color: string }) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={color}>{text}</Text>\n </Box>\n );\n}\n\nfunction SummaryView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n summary: { wordCount: number; errors: number; durationMs: number; perWordErrors: Record<string, number> };\n}) {\n const { summary } = props;\n const minutes = summary.durationMs / 60000;\n const wpm = minutes > 0 ? Math.round((summary.wordCount / minutes) * 10) / 10 : 0;\n const errorWords = Object.keys(summary.perWordErrors).length;\n const acc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - errorWords) / summary.wordCount);\n const accPct = Math.round(acc * 1000) / 10;\n\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const name = truncateName(props.dictName, 20);\n const subtitle =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName}`;\n\n const nextLabel =\n props.mode === 'loop'\n ? t.practice.summary.loopAgain\n : props.mode === 'review' || props.chapterIndex + 1 >= props.totalChapters\n ? t.practice.summary.backMenu\n : t.practice.summary.nextChapter;\n const footer = `Enter ${nextLabel} · m ${t.practice.summary.reviewMistakes} · Esc ${t.practice.summary.backMenu}`;\n\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.success}>\n {t.practice.chapterComplete}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n\n <Box marginTop={3} flexDirection=\"row\" justifyContent=\"center\">\n <StatCard label={t.practice.statCards.words} value={String(summary.wordCount)} color={PALETTE.text} />\n <StatCard\n label={t.practice.statCards.errors}\n value={String(summary.errors)}\n color={summary.errors > 0 ? PALETTE.error : PALETTE.muted}\n />\n <StatCard label={t.practice.statCards.wpm} value={String(wpm)} color={PALETTE.accent} />\n <StatCard label={t.practice.statCards.accuracy} value={`${accPct}%`} color={PALETTE.accent} />\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.statCards.elapsed(fmtTime(summary.durationMs))}</Text>\n </Box>\n\n <Box flexGrow={1} />\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{footer}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction StatCard({ label, value, color }: { label: string; value: string; color: string }) {\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" marginX={3}>\n <Text bold color={color}>{value}</Text>\n <Text color={PALETTE.muted}>{label}</Text>\n </Box>\n );\n}\n","export function shuffle<T>(arr: readonly T[], rng: () => number = Math.random): T[] {\n const out = [...arr];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(rng() * (i + 1));\n const tmp = out[i]!;\n out[i] = out[j]!;\n out[j] = tmp;\n }\n return out;\n}\n\nexport function mulberry32(seed: number): () => number {\n let t = seed >>> 0;\n return () => {\n t = (t + 0x6d2b79f5) >>> 0;\n let r = Math.imul(t ^ (t >>> 15), 1 | t);\n r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;\n return ((r ^ (r >>> 14)) >>> 0) / 4294967296;\n };\n}\n","import type { Word } from './dictionary.js';\nimport { shuffle, mulberry32 } from '../util/shuffle.js';\n\nexport type Mode = 'order' | 'dictation' | 'review' | 'random' | 'loop';\n\nexport function chunkChapters(words: Word[], chapterSize: number): Word[][] {\n if (chapterSize <= 0) throw new Error('chapterSize must be positive');\n const chunks: Word[][] = [];\n for (let i = 0; i < words.length; i += chapterSize) {\n chunks.push(words.slice(i, i + chapterSize));\n }\n return chunks;\n}\n\nexport function chapterCount(totalWords: number, chapterSize: number): number {\n return Math.ceil(totalWords / chapterSize);\n}\n\n/**\n * Build a play list for a chapter, applying the given mode.\n * - order: return as-is\n * - dictation: same as order (hide-the-word behavior is UI-only)\n * - random: single shuffled pass\n * - loop: return as-is; the practice screen drives the repeat\n * - review: caller passes the mistake-book words, we just chunk those\n */\nexport function buildPlaylist(chapter: Word[], mode: Mode, seed?: number): Word[] {\n if (mode === 'random') {\n const rng = seed === undefined ? Math.random : mulberry32(seed);\n return shuffle(chapter, rng);\n }\n return chapter;\n}\n","export type InputState = {\n target: string;\n typed: string;\n errorsThisWord: number;\n};\n\nexport type InputEvent =\n | { type: 'char'; ch: string }\n | { type: 'backspace' }\n | { type: 'reset' };\n\nexport type InputEffect = 'none' | 'progress' | 'wrong' | 'correct' | 'skipped';\n\nexport function initialState(target: string): InputState {\n return { target, typed: '', errorsThisWord: 0 };\n}\n\nexport function reduce(state: InputState, ev: InputEvent): { state: InputState; effect: InputEffect } {\n switch (ev.type) {\n case 'reset':\n return { state: { ...state, typed: '' }, effect: 'none' };\n case 'backspace': {\n if (state.typed.length === 0) return { state, effect: 'none' };\n return { state: { ...state, typed: state.typed.slice(0, -1) }, effect: 'none' };\n }\n case 'char': {\n const candidate = state.typed + ev.ch;\n // Compare by code-point index, not byte length, to handle unicode safely.\n const targetUpToCandidate = [...state.target].slice(0, [...candidate].length).join('');\n if (candidate === targetUpToCandidate) {\n if (candidate.length === state.target.length) {\n return { state: { ...state, typed: candidate }, effect: 'correct' };\n }\n return { state: { ...state, typed: candidate }, effect: 'progress' };\n }\n return {\n state: { ...state, typed: '', errorsThisWord: state.errorsThisWord + 1 },\n effect: 'wrong',\n };\n }\n }\n}\n","import type { Word } from './dictionary.js';\nimport { initialState, reduce, type InputEvent, type InputState, type InputEffect } from './input-buffer.js';\n\nexport type SessionWordResult = { word: string; errors: number; durationMs: number; skipped?: boolean };\n\nexport type Session = {\n startedAt: number;\n results: SessionWordResult[];\n current: { wordIndex: number; wordStartedAt: number; input: InputState } | null;\n finishedAt: number | null;\n playlist: Word[];\n};\n\nexport function startSession(playlist: Word[], now = Date.now()): Session {\n if (playlist.length === 0) {\n return { startedAt: now, results: [], current: null, finishedAt: now, playlist };\n }\n return {\n startedAt: now,\n results: [],\n current: { wordIndex: 0, wordStartedAt: now, input: initialState(playlist[0]!.name) },\n finishedAt: null,\n playlist,\n };\n}\n\nexport function feedSession(session: Session, ev: InputEvent, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const { state, effect } = reduce(session.current.input, ev);\n if (effect === 'correct') {\n const finished: SessionWordResult = {\n word: state.target,\n errors: state.errorsThisWord,\n durationMs: now - session.current.wordStartedAt,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, finished];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect,\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect,\n };\n }\n return {\n session: {\n ...session,\n current: { ...session.current, input: state },\n },\n effect,\n };\n}\n\nexport function skipSession(session: Session, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const result: SessionWordResult = {\n word: session.current.input.target,\n errors: 0,\n durationMs: now - session.current.wordStartedAt,\n skipped: true,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, result];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect: 'skipped',\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect: 'skipped',\n };\n}\n\nexport function sessionSummary(session: Session): {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n} {\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const durationMs =\n (session.finishedAt ?? Date.now()) - session.startedAt;\n const perWordErrors: Record<string, number> = {};\n for (const r of session.results) {\n if (r.errors > 0) perWordErrors[r.word] = (perWordErrors[r.word] ?? 0) + r.errors;\n }\n return { wordCount: session.results.length, errors, durationMs, perWordErrors };\n}\n","import { useEffect, useReducer, useRef, useState } from 'react';\nimport { useInput, useApp } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport { startSession, feedSession, skipSession, type Session } from '../../domain/session.js';\nimport type { InputEffect } from '../../domain/input-buffer.js';\n\ntype Action =\n | { type: 'event'; input: string; key: { backspace?: boolean; delete?: boolean; tab?: boolean; escape?: boolean; return?: boolean; ctrl?: boolean }; now: number }\n | { type: 'start'; playlist: Word[]; now: number }\n | { type: 'skip'; now: number };\n\n// effectSeq is a monotonic counter bumped on every dispatch that resolves into\n// a new lastEffect value. Consumers (PracticeScreen audio effect) depend on it\n// instead of lastEffect alone because React's Object.is comparison treats two\n// consecutive 'progress' strings as equal — without a counter, useEffect would\n// fire on only the first char of a word, dropping the keystroke sound on every\n// subsequent character.\ntype LoopState = { session: Session; lastEffect: InputEffect | null; effectSeq: number };\n\nfunction reducer(state: LoopState, action: Action): LoopState {\n if (action.type === 'start') {\n return { session: startSession(action.playlist, action.now), lastEffect: null, effectSeq: 0 };\n }\n if (action.type === 'skip') {\n const r = skipSession(state.session, action.now);\n return { session: r.session, lastEffect: r.effect, effectSeq: state.effectSeq + 1 };\n }\n if (action.type === 'event') {\n if (action.key.backspace || action.key.delete) {\n const r = feedSession(state.session, { type: 'backspace' }, action.now);\n return { session: r.session, lastEffect: r.effect, effectSeq: state.effectSeq + 1 };\n }\n if (action.input.length === 0) return state;\n let session = state.session;\n let lastEffect: InputEffect | null = state.lastEffect;\n for (const c of action.input) {\n const r = feedSession(session, { type: 'char', ch: c }, action.now);\n session = r.session;\n lastEffect = r.effect;\n if (session.finishedAt !== null) break;\n }\n return { session, lastEffect, effectSeq: state.effectSeq + 1 };\n }\n return state;\n}\n\n// Classify a typed batch from Ink's useInput. Single source of truth so the\n// IME-detection logic stays unit-testable without rendering Ink.\n// kind='ime' → contains at least one non-ASCII codepoint (>= 0x80, e.g.\n// CJK / Latin-1 supplement); reject the batch and show hint.\n// kind='valid' → all-ASCII printable; `cleaned` has the dispatchable subset.\n// kind='noise' → ASCII control (DEL, NUL, etc.) or empty; ignore silently.\nexport type InputBatchKind = 'ime' | 'valid' | 'noise';\nexport function classifyInputBatch(input: string): { kind: InputBatchKind; cleaned: string } {\n const chars = [...input];\n if (chars.some((c) => c.codePointAt(0)! >= 0x80)) {\n return { kind: 'ime', cleaned: '' };\n }\n const cleaned = chars\n .filter((c) => {\n const cp = c.codePointAt(0)!;\n return cp >= 0x20 && cp <= 0x7e;\n })\n .join('');\n return { kind: cleaned.length > 0 ? 'valid' : 'noise', cleaned };\n}\n\nexport type UseWordLoopOpts = {\n playlist: Word[];\n onComplete: (session: Session) => void;\n onTab?: () => void;\n onEscape?: () => void;\n onSkip?: () => void;\n // Fires when the typed batch contains any codepoint > 0x7E — almost always\n // means the user's IME is active (CJK input method). The batch is rejected\n // entirely so the wrong characters aren't fed to the session.\n onImeBlock?: () => void;\n // Fires when a non-empty all-ASCII batch is about to dispatch. Used to\n // clear a previously-set IME warning so it auto-dismisses on the first\n // successful keystroke.\n onValidInput?: () => void;\n enabled?: boolean;\n};\n\nexport function useWordLoop({\n playlist,\n onComplete,\n onTab,\n onEscape,\n onSkip,\n onImeBlock,\n onValidInput,\n enabled = true,\n}: UseWordLoopOpts) {\n const [state, dispatch] = useReducer(reducer, undefined, () => ({\n session: startSession(playlist, Date.now()),\n lastEffect: null as InputEffect | null,\n effectSeq: 0,\n }));\n const completedRef = useRef(false);\n const [tick, setTick] = useState(0);\n const { exit } = useApp();\n\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n exit();\n return;\n }\n if (key.ctrl && input === 'n') {\n onSkip?.();\n dispatch({ type: 'skip', now: Date.now() });\n return;\n }\n if (key.escape) {\n onEscape?.();\n return;\n }\n if (key.tab) {\n onTab?.();\n return;\n }\n if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return) return;\n if (key.ctrl || key.meta) return;\n const { kind, cleaned } = classifyInputBatch(input);\n if (kind === 'ime') {\n onImeBlock?.();\n return;\n }\n if (kind === 'noise') return;\n onValidInput?.();\n dispatch({ type: 'event', input: cleaned, key, now: Date.now() });\n },\n { isActive: enabled },\n );\n\n useEffect(() => {\n if (state.session.finishedAt !== null && !completedRef.current) {\n completedRef.current = true;\n onComplete(state.session);\n }\n }, [state.session, onComplete]);\n\n useEffect(() => {\n if (state.session.finishedAt !== null) return;\n const id = setInterval(() => setTick((t) => t + 1), 1000);\n return () => clearInterval(id);\n }, [state.session.finishedAt]);\n\n return { session: state.session, lastEffect: state.lastEffect, effectSeq: state.effectSeq, tick };\n}\n\n// Exposed for unit tests only — verifies effectSeq monotonic increment and\n// effect dispatch behavior without mounting a full Ink tree.\nexport const __test = { reducer };\n","import { useEffect, useRef } from 'react';\nimport {\n initAudio,\n playCorrect,\n playWrong,\n playKeystroke,\n playPronunciation,\n prefetchPronunciation,\n} from '../../infra/audio.js';\n\ntype Opts = {\n enabled: boolean;\n accent: 'us' | 'uk';\n autoplayPronunciation: boolean;\n pronunciationRate: number;\n pronunciationSource: 'youdao' | 'dictapi';\n};\n\nexport type AudioControls = {\n keystroke: () => void;\n correct: () => void;\n wrong: () => void;\n pronounce: (word: string) => void;\n prefetch: (word: string) => void;\n};\n\nexport function useAudio(opts: Opts): AudioControls {\n const initedRef = useRef(false);\n useEffect(() => {\n if (initedRef.current) return;\n initedRef.current = true;\n initAudio(!opts.enabled).catch(() => undefined);\n }, [opts.enabled]);\n\n return {\n keystroke: () => opts.enabled && playKeystroke(),\n correct: () => opts.enabled && playCorrect(),\n wrong: () => opts.enabled && playWrong(),\n pronounce: (word) => {\n if (!opts.enabled) return;\n if (opts.autoplayPronunciation)\n void playPronunciation(word, opts.accent, opts.pronunciationRate, opts.pronunciationSource);\n },\n prefetch: (word) => {\n if (!opts.enabled) return;\n void prefetchPronunciation(word, opts.accent, opts.pronunciationSource);\n },\n };\n}\n","import { useCallback } from 'react';\nimport { appendSession, type SessionRecord } from '../../domain/stats.js';\nimport { loadMistakes, saveMistakes, bump } from '../../domain/mistakes.js';\nimport { addChapter as trackChapter } from '../../infra/session-tracker.js';\nimport type { Mode } from '../../domain/chapters.js';\n\ntype Summary = {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n};\n\nexport function useSessionPersistence(meta: { dictId: string; chapterIndex: number; mode: Mode }) {\n return useCallback(\n async (summary: Summary): Promise<void> => {\n const rec: SessionRecord = {\n ts: new Date().toISOString(),\n dictId: meta.dictId,\n chapter: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n };\n await appendSession(rec);\n trackChapter({\n dictId: meta.dictId,\n chapterIndex: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n });\n const dirty = Object.entries(summary.perWordErrors).filter(([, n]) => n > 0);\n if (dirty.length === 0) return;\n let book = await loadMistakes();\n for (const [word, n] of dirty) book = bump(book, word, meta.dictId, n);\n await saveMistakes(book);\n },\n [meta.dictId, meta.chapterIndex, meta.mode],\n );\n}\n","// Detect terminal capabilities relevant to BigWord rendering.\n//\n// VT100 DECDWL/DECDHL (ESC#3 / ESC#4 / ESC#6) let us render the practice\n// word using the terminal's real font at 2× width and 2× height. Support\n// varies: macOS Terminal.app, Windows Terminal, iTerm2, WezTerm, xterm,\n// mintty, Alacritty render correctly; kitty / VTE-based / Konsole either\n// skip the attribute or render glitchy output.\n//\n// Detection is env-only — runtime device-attribute queries (ESC[c) would\n// race against Ink's input pipeline and aren't worth the complexity.\n\nexport type TermCaps = {\n supportsDoubleHeight: boolean;\n};\n\nexport function detectTermCaps(env: NodeJS.ProcessEnv = process.env): TermCaps {\n if (env.KITTY_WINDOW_ID) return { supportsDoubleHeight: false };\n if (env.VTE_VERSION) return { supportsDoubleHeight: false };\n if (env.KONSOLE_VERSION) return { supportsDoubleHeight: false };\n\n if (env.WT_SESSION) return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'Apple_Terminal') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'iTerm.app') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'WezTerm') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'mintty') return { supportsDoubleHeight: true };\n if (env.ALACRITTY_SOCKET || env.ALACRITTY_LOG) return { supportsDoubleHeight: true };\n if (env.XTERM_VERSION) return { supportsDoubleHeight: true };\n\n return { supportsDoubleHeight: false };\n}\n","import { Box, Text, useStdout } from 'ink';\nimport { PALETTE } from './BigWord.js';\n\n// Renders the practice word using VT100 DECDHL (double-height + double-width\n// line attribute). The terminal draws the SAME glyphs as a single-cell line\n// but at 2× scale — real terminal font, no rasterization, no figlet/ASCII art.\n//\n// DECDHL requires two physical rows with identical content: ESC#3 for the\n// top half, ESC#4 for the bottom half. The terminal merges them into one\n// visual 2×-tall glyph row. So we emit 2 Ink rows here.\n//\n// Width math:\n// each character occupies 2 visual cells under DECDHL\n// padding chars are also doubled (DECDHL is a line attribute, applies to all)\n// so actual_pad_chars = visual_pad / 2\n//\n// Ink's width measurement (via string-width) strips the ESC#3 prefix to 0\n// columns but counts the raw spaces of `padding`; we don't constrain the\n// container width so Ink's miscount is harmless.\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nconst SGR_CACHE = new Map<string, string>();\n\nfunction sgrBold(hex: string): string {\n const cached = SGR_CACHE.get(hex);\n if (cached) return cached;\n const r = parseInt(hex.slice(1, 3), 16);\n const g = parseInt(hex.slice(3, 5), 16);\n const b = parseInt(hex.slice(5, 7), 16);\n const sgr = `\\x1b[1;38;2;${r};${g};${b}m`;\n SGR_CACHE.set(hex, sgr);\n return sgr;\n}\n\nconst RESET = '\\x1b[0m';\n\nexport function BigWordHuge({ target, typed, error = false, hideTarget = false }: Props) {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n const chars = [...target];\n const typedChars = [...typed];\n\n const visualWidth = chars.length * 2;\n const visualPad = Math.max(0, Math.floor((cols - visualWidth) / 2));\n const actualPad = ' '.repeat(Math.floor(visualPad / 2));\n\n const body = chars\n .map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return `${sgrBold(color)}${display}${RESET}`;\n })\n .join('');\n\n const topLine = `\\x1b#3${actualPad}${body}`;\n const bottomLine = `\\x1b#4${actualPad}${body}`;\n\n return (\n <Box flexDirection=\"column\" paddingY={3} width=\"100%\">\n <Text>{topLine}</Text>\n <Text>{bottomLine}</Text>\n </Box>\n );\n}\n","import { useAppState } from '../app-state.js';\nimport { detectTermCaps, type TermCaps } from '../../util/term-caps.js';\nimport { BigWord } from './BigWord.js';\nimport { BigWordHuge } from './BigWordHuge.js';\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nlet cachedCaps: TermCaps | null = null;\nfunction getCaps(): TermCaps {\n if (!cachedCaps) cachedCaps = detectTermCaps();\n return cachedCaps;\n}\n\nexport function BigWordAuto(props: Props) {\n const { cfg } = useAppState();\n const cols = process.stdout?.columns ?? 80;\n const caps = getCaps();\n\n const wantsHuge =\n cfg.wordDisplay === 'huge' ||\n (cfg.wordDisplay === 'auto' && caps.supportsDoubleHeight);\n const fits = cols >= 2 * [...props.target].length + 4;\n\n return wantsHuge && fits ? <BigWordHuge {...props} /> : <BigWord {...props} />;\n}\n","import type { ReactNode } from 'react';\nimport { Box, Text, useStdout } from 'ink';\nimport { PALETTE } from '../components/BigWord.js';\nimport { useStrings } from '../../i18n/context.js';\n\nconst RIGHT_WIDTH = 28;\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction useLeftWidth(): number {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n return Math.max(20, cols - RIGHT_WIDTH);\n}\n\nfunction Row({ left, right }: { left: ReactNode; right: ReactNode }) {\n const leftWidth = useLeftWidth();\n return (\n <Box>\n <Box width={leftWidth}>{left}</Box>\n <Box width={RIGHT_WIDTH} justifyContent=\"flex-end\">\n {right}\n </Box>\n </Box>\n );\n}\n\nexport function StealthTyping(props: {\n target: string;\n typed: string;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n error: boolean;\n imeBlocked: boolean;\n audioWarning: string | null;\n info: {\n visible: boolean;\n dictName: string;\n chapterLabel: string;\n completed: number;\n total: number;\n wpm: number;\n accPct: number;\n elapsedMs: number;\n };\n}) {\n const t = useStrings();\n const target = [...props.target];\n const typed = [...props.typed];\n\n // Row 1: word chars + inline phonetic (two-space gap, italic + dim muted).\n // error=true flashes the entire word red; phonetic remains dim regardless.\n const wordCell = (\n <Box>\n {target.map((ch, i) => {\n const isTyped = i < typed.length;\n const display = props.hideTarget && !isTyped ? '_' : isTyped ? typed[i]! : ch;\n const color = props.error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n {props.phonetic && (\n <>\n <Text> </Text>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </>\n )}\n </Box>\n );\n\n // Row 3: first translation in primary cyan (matches TypingLayout translation color)\n const translationCell = props.translation.length > 0 ? (\n <Text color={PALETTE.primary}>{props.translation[0]!}</Text>\n ) : (\n <Text> </Text>\n );\n\n const info = props.info;\n const accFmt = Number.isInteger(info.accPct) ? `${info.accPct}` : info.accPct.toFixed(1);\n\n // Right column priority: imeBlocked > audioWarning > info > idle.\n // imeBlocked: warning indicator on row 1, rows 2/3 blank.\n // audioWarning (and no IME issue): `! audio` short marker on row 1, rows 2/3 blank.\n // info.visible: dict / progress / time on rows 1/2/3.\n // idle: all three rows blank.\n const showAudio = !props.imeBlocked && props.audioWarning !== null;\n const right1 = props.imeBlocked ? (\n <Text color={PALETTE.warning}>{t.practice.imeWarningShort}</Text>\n ) : showAudio ? (\n <Text color={PALETTE.warning}>{t.practice.audioWarningShort}</Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.dictName} · ${info.chapterLabel}`}</Text>\n ) : (\n <Text> </Text>\n );\n const right2 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.completed}/${info.total} · ${info.wpm}wpm · ${accFmt}%`}</Text>\n ) : (\n <Text> </Text>\n );\n const right3 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{fmtTime(info.elapsedMs)}</Text>\n ) : (\n <Text> </Text>\n );\n\n // Layout: 3 rendered rows. Middle row's left is blank → acts as a 1-line\n // visual gap between word+phonetic and translation. Right column keeps all\n // three info slots (dict / progress / time) when info.visible is true.\n return (\n <Box flexDirection=\"column\">\n <Row left={wordCell} right={right1} />\n <Row left={<Text> </Text>} right={right2} />\n <Row left={translationCell} right={right3} />\n </Box>\n );\n}\n\nexport function StealthPaused() {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.warning}>{t.stealth.paused}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.pausedHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n\nexport function StealthSummary(props: {\n wordCount: number;\n errors: number;\n durationMs: number;\n wpm: number;\n accPct: number;\n}) {\n const t = useStrings();\n const accFmt = Number.isInteger(props.accPct) ? `${props.accPct}` : props.accPct.toFixed(1);\n const line = `${t.stealth.chapterDone} · ${props.wordCount}w · ${props.wpm}wpm · ${accFmt}% · ${fmtTime(props.durationMs)}`;\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.success}>{line}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.nextHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n"],"mappings":"8fAAA,OAAS,YAAAA,EAAU,aAAAC,EAAW,UAAAC,OAAc,QAC5C,OAAS,OAAAC,EAAK,QAAAC,EAAM,UAAAC,GAAQ,YAAAC,MAAgB,MCDrC,SAASC,GAAWC,EAAmBC,EAAoB,KAAK,OAAa,CAClF,IAAMC,EAAM,CAAC,GAAGF,CAAG,EACnB,QAASG,EAAID,EAAI,OAAS,EAAGC,EAAI,EAAGA,IAAK,CACvC,IAAMC,EAAI,KAAK,MAAMH,EAAI,GAAKE,EAAI,EAAE,EAC9BE,EAAMH,EAAIC,CAAC,EACjBD,EAAIC,CAAC,EAAID,EAAIE,CAAC,EACdF,EAAIE,CAAC,EAAIC,CACX,CACA,OAAOH,CACT,CAEO,SAASI,GAAWC,EAA4B,CACrD,IAAIC,EAAID,IAAS,EACjB,MAAO,IAAM,CACXC,EAAKA,EAAI,aAAgB,EACzB,IAAI,EAAI,KAAK,KAAKA,EAAKA,IAAM,GAAK,EAAIA,CAAC,EACvC,SAAK,EAAI,KAAK,KAAK,EAAK,IAAM,EAAI,GAAK,CAAC,EAAK,IACpC,EAAK,IAAM,MAAS,GAAK,UACpC,CACF,CCdO,SAASC,GAAcC,EAAeC,EAA+B,CAC1E,GAAIA,GAAe,EAAG,MAAM,IAAI,MAAM,8BAA8B,EACpE,IAAMC,EAAmB,CAAC,EAC1B,QAASC,EAAI,EAAGA,EAAIH,EAAM,OAAQG,GAAKF,EACrCC,EAAO,KAAKF,EAAM,MAAMG,EAAGA,EAAIF,CAAW,CAAC,EAE7C,OAAOC,CACT,CAcO,SAASE,GAAcC,EAAiBC,EAAYC,EAAuB,CAChF,GAAID,IAAS,SAAU,CACrB,IAAME,EAAMD,IAAS,OAAY,KAAK,OAASE,GAAWF,CAAI,EAC9D,OAAOG,GAAQL,EAASG,CAAG,CAC7B,CACA,OAAOH,CACT,CCnBO,SAASM,EAAaC,EAA4B,CACvD,MAAO,CAAE,OAAAA,EAAQ,MAAO,GAAI,eAAgB,CAAE,CAChD,CAEO,SAASC,GAAOC,EAAmBC,EAA4D,CACpG,OAAQA,EAAG,KAAM,CACf,IAAK,QACH,MAAO,CAAE,MAAO,CAAE,GAAGD,EAAO,MAAO,EAAG,EAAG,OAAQ,MAAO,EAC1D,IAAK,YACH,OAAIA,EAAM,MAAM,SAAW,EAAU,CAAE,MAAAA,EAAO,OAAQ,MAAO,EACtD,CAAE,MAAO,CAAE,GAAGA,EAAO,MAAOA,EAAM,MAAM,MAAM,EAAG,EAAE,CAAE,EAAG,OAAQ,MAAO,EAEhF,IAAK,OAAQ,CACX,IAAME,EAAYF,EAAM,MAAQC,EAAG,GAE7BE,EAAsB,CAAC,GAAGH,EAAM,MAAM,EAAE,MAAM,EAAG,CAAC,GAAGE,CAAS,EAAE,MAAM,EAAE,KAAK,EAAE,EACrF,OAAIA,IAAcC,EACZD,EAAU,SAAWF,EAAM,OAAO,OAC7B,CAAE,MAAO,CAAE,GAAGA,EAAO,MAAOE,CAAU,EAAG,OAAQ,SAAU,EAE7D,CAAE,MAAO,CAAE,GAAGF,EAAO,MAAOE,CAAU,EAAG,OAAQ,UAAW,EAE9D,CACL,MAAO,CAAE,GAAGF,EAAO,MAAO,GAAI,eAAgBA,EAAM,eAAiB,CAAE,EACvE,OAAQ,OACV,CACF,CACF,CACF,CC5BO,SAASI,EAAaC,EAAkBC,EAAM,KAAK,IAAI,EAAY,CACxE,OAAID,EAAS,SAAW,EACf,CAAE,UAAWC,EAAK,QAAS,CAAC,EAAG,QAAS,KAAM,WAAYA,EAAK,SAAAD,CAAS,EAE1E,CACL,UAAWC,EACX,QAAS,CAAC,EACV,QAAS,CAAE,UAAW,EAAG,cAAeA,EAAK,MAAOC,EAAaF,EAAS,CAAC,EAAG,IAAI,CAAE,EACpF,WAAY,KACZ,SAAAA,CACF,CACF,CAEO,SAASG,EAAYC,EAAkBC,EAAgBJ,EAAM,KAAK,IAAI,EAA8C,CACzH,GAAI,CAACG,EAAQ,QAAS,MAAO,CAAE,QAAAA,EAAS,OAAQ,MAAO,EACvD,GAAM,CAAE,MAAAE,EAAO,OAAAC,CAAO,EAAIC,GAAOJ,EAAQ,QAAQ,MAAOC,CAAE,EAC1D,GAAIE,IAAW,UAAW,CACxB,IAAME,EAA8B,CAClC,KAAMH,EAAM,OACZ,OAAQA,EAAM,eACd,WAAYL,EAAMG,EAAQ,QAAQ,aACpC,EACMM,EAAYN,EAAQ,QAAQ,UAAY,EACxCO,EAAU,CAAC,GAAGP,EAAQ,QAASK,CAAQ,EAC7C,OAAIC,GAAaN,EAAQ,SAAS,OACzB,CACL,QAAS,CAAE,GAAGA,EAAS,QAAAO,EAAS,QAAS,KAAM,WAAYV,CAAI,EAC/D,OAAAM,CACF,EAEK,CACL,QAAS,CACP,GAAGH,EACH,QAAAO,EACA,QAAS,CACP,UAAWD,EACX,cAAeT,EACf,MAAOC,EAAaE,EAAQ,SAASM,CAAS,EAAG,IAAI,CACvD,CACF,EACA,OAAAH,CACF,CACF,CACA,MAAO,CACL,QAAS,CACP,GAAGH,EACH,QAAS,CAAE,GAAGA,EAAQ,QAAS,MAAOE,CAAM,CAC9C,EACA,OAAAC,CACF,CACF,CAEO,SAASK,GAAYR,EAAkBH,EAAM,KAAK,IAAI,EAA8C,CACzG,GAAI,CAACG,EAAQ,QAAS,MAAO,CAAE,QAAAA,EAAS,OAAQ,MAAO,EACvD,IAAMS,EAA4B,CAChC,KAAMT,EAAQ,QAAQ,MAAM,OAC5B,OAAQ,EACR,WAAYH,EAAMG,EAAQ,QAAQ,cAClC,QAAS,EACX,EACMM,EAAYN,EAAQ,QAAQ,UAAY,EACxCO,EAAU,CAAC,GAAGP,EAAQ,QAASS,CAAM,EAC3C,OAAIH,GAAaN,EAAQ,SAAS,OACzB,CACL,QAAS,CAAE,GAAGA,EAAS,QAAAO,EAAS,QAAS,KAAM,WAAYV,CAAI,EAC/D,OAAQ,SACV,EAEK,CACL,QAAS,CACP,GAAGG,EACH,QAAAO,EACA,QAAS,CACP,UAAWD,EACX,cAAeT,EACf,MAAOC,EAAaE,EAAQ,SAASM,CAAS,EAAG,IAAI,CACvD,CACF,EACA,OAAQ,SACV,CACF,CAEO,SAASI,EAAeV,EAK7B,CACA,IAAMW,EAASX,EAAQ,QAAQ,OAAO,CAACY,EAAGC,IAAMD,EAAIC,EAAE,OAAQ,CAAC,EACzDC,GACHd,EAAQ,YAAc,KAAK,IAAI,GAAKA,EAAQ,UACzCe,EAAwC,CAAC,EAC/C,QAAWF,KAAKb,EAAQ,QAClBa,EAAE,OAAS,IAAGE,EAAcF,EAAE,IAAI,GAAKE,EAAcF,EAAE,IAAI,GAAK,GAAKA,EAAE,QAE7E,MAAO,CAAE,UAAWb,EAAQ,QAAQ,OAAQ,OAAAW,EAAQ,WAAAG,EAAY,cAAAC,CAAc,CAChF,CC7GA,OAAS,aAAAC,GAAW,cAAAC,GAAY,UAAAC,GAAQ,YAAAC,OAAgB,QACxD,OAAS,YAAAC,GAAU,UAAAC,OAAc,MAkBjC,SAASC,GAAQC,EAAkBC,EAA2B,CAC5D,GAAIA,EAAO,OAAS,QAClB,MAAO,CAAE,QAASC,EAAaD,EAAO,SAAUA,EAAO,GAAG,EAAG,WAAY,KAAM,UAAW,CAAE,EAE9F,GAAIA,EAAO,OAAS,OAAQ,CAC1B,IAAM,EAAIE,GAAYH,EAAM,QAASC,EAAO,GAAG,EAC/C,MAAO,CAAE,QAAS,EAAE,QAAS,WAAY,EAAE,OAAQ,UAAWD,EAAM,UAAY,CAAE,CACpF,CACA,GAAIC,EAAO,OAAS,QAAS,CAC3B,GAAIA,EAAO,IAAI,WAAaA,EAAO,IAAI,OAAQ,CAC7C,IAAMG,EAAIC,EAAYL,EAAM,QAAS,CAAE,KAAM,WAAY,EAAGC,EAAO,GAAG,EACtE,MAAO,CAAE,QAASG,EAAE,QAAS,WAAYA,EAAE,OAAQ,UAAWJ,EAAM,UAAY,CAAE,CACpF,CACA,GAAIC,EAAO,MAAM,SAAW,EAAG,OAAOD,EACtC,IAAIM,EAAUN,EAAM,QAChBO,EAAiCP,EAAM,WAC3C,QAAWQ,KAAKP,EAAO,MAAO,CAC5B,IAAMG,EAAIC,EAAYC,EAAS,CAAE,KAAM,OAAQ,GAAIE,CAAE,EAAGP,EAAO,GAAG,EAGlE,GAFAK,EAAUF,EAAE,QACZG,EAAaH,EAAE,OACXE,EAAQ,aAAe,KAAM,KACnC,CACA,MAAO,CAAE,QAAAA,EAAS,WAAAC,EAAY,UAAWP,EAAM,UAAY,CAAE,CAC/D,CACA,OAAOA,CACT,CASO,SAASS,GAAmBC,EAA0D,CAC3F,IAAMC,EAAQ,CAAC,GAAGD,CAAK,EACvB,GAAIC,EAAM,KAAMH,GAAMA,EAAE,YAAY,CAAC,GAAM,GAAI,EAC7C,MAAO,CAAE,KAAM,MAAO,QAAS,EAAG,EAEpC,IAAMI,EAAUD,EACb,OAAQH,GAAM,CACb,IAAMK,EAAKL,EAAE,YAAY,CAAC,EAC1B,OAAOK,GAAM,IAAQA,GAAM,GAC7B,CAAC,EACA,KAAK,EAAE,EACV,MAAO,CAAE,KAAMD,EAAQ,OAAS,EAAI,QAAU,QAAS,QAAAA,CAAQ,CACjE,CAmBO,SAASE,GAAY,CAC1B,SAAAC,EACA,WAAAC,EACA,MAAAC,EACA,SAAAC,EACA,OAAAC,EACA,WAAAC,EACA,aAAAC,EACA,QAAAC,EAAU,EACZ,EAAoB,CAClB,GAAM,CAACtB,EAAOuB,CAAQ,EAAIC,GAAWzB,GAAS,OAAW,KAAO,CAC9D,QAASG,EAAaa,EAAU,KAAK,IAAI,CAAC,EAC1C,WAAY,KACZ,UAAW,CACb,EAAE,EACIU,EAAeC,GAAO,EAAK,EAC3B,CAACC,EAAMC,CAAO,EAAIC,GAAS,CAAC,EAC5B,CAAE,KAAAC,CAAK,EAAIC,GAAO,EAExB,OAAAC,GACE,CAACtB,EAAOuB,IAAQ,CACd,GAAIA,EAAI,MAAQvB,IAAU,IAAK,CAC7BoB,EAAK,EACL,MACF,CACA,GAAIG,EAAI,MAAQvB,IAAU,IAAK,CAC7BS,IAAS,EACTI,EAAS,CAAE,KAAM,OAAQ,IAAK,KAAK,IAAI,CAAE,CAAC,EAC1C,MACF,CACA,GAAIU,EAAI,OAAQ,CACdf,IAAW,EACX,MACF,CACA,GAAIe,EAAI,IAAK,CACXhB,IAAQ,EACR,MACF,CAEA,GADIgB,EAAI,SAAWA,EAAI,WAAaA,EAAI,WAAaA,EAAI,YAAcA,EAAI,QACvEA,EAAI,MAAQA,EAAI,KAAM,OAC1B,GAAM,CAAE,KAAAC,EAAM,QAAAtB,CAAQ,EAAIH,GAAmBC,CAAK,EAClD,GAAIwB,IAAS,MAAO,CAClBd,IAAa,EACb,MACF,CACIc,IAAS,UACbb,IAAe,EACfE,EAAS,CAAE,KAAM,QAAS,MAAOX,EAAS,IAAAqB,EAAK,IAAK,KAAK,IAAI,CAAE,CAAC,EAClE,EACA,CAAE,SAAUX,CAAQ,CACtB,EAEAa,GAAU,IAAM,CACVnC,EAAM,QAAQ,aAAe,MAAQ,CAACyB,EAAa,UACrDA,EAAa,QAAU,GACvBT,EAAWhB,EAAM,OAAO,EAE5B,EAAG,CAACA,EAAM,QAASgB,CAAU,CAAC,EAE9BmB,GAAU,IAAM,CACd,GAAInC,EAAM,QAAQ,aAAe,KAAM,OACvC,IAAMoC,EAAK,YAAY,IAAMR,EAASS,GAAMA,EAAI,CAAC,EAAG,GAAI,EACxD,MAAO,IAAM,cAAcD,CAAE,CAC/B,EAAG,CAACpC,EAAM,QAAQ,UAAU,CAAC,EAEtB,CAAE,QAASA,EAAM,QAAS,WAAYA,EAAM,WAAY,UAAWA,EAAM,UAAW,KAAA2B,CAAK,CAClG,CCtJA,OAAS,aAAAW,GAAW,UAAAC,OAAc,QA0B3B,SAASC,GAASC,EAA2B,CAClD,IAAMC,EAAYC,GAAO,EAAK,EAC9B,OAAAC,GAAU,IAAM,CACVF,EAAU,UACdA,EAAU,QAAU,GACpBG,GAAU,CAACJ,EAAK,OAAO,EAAE,MAAM,IAAG,EAAY,EAChD,EAAG,CAACA,EAAK,OAAO,CAAC,EAEV,CACL,UAAW,IAAMA,EAAK,SAAWK,GAAc,EAC/C,QAAS,IAAML,EAAK,SAAWM,GAAY,EAC3C,MAAO,IAAMN,EAAK,SAAWO,GAAU,EACvC,UAAYC,GAAS,CACdR,EAAK,SACNA,EAAK,uBACFS,GAAkBD,EAAMR,EAAK,OAAQA,EAAK,kBAAmBA,EAAK,mBAAmB,CAC9F,EACA,SAAWQ,GAAS,CACbR,EAAK,SACLU,GAAsBF,EAAMR,EAAK,OAAQA,EAAK,mBAAmB,CACxE,CACF,CACF,CChDA,OAAS,eAAAW,OAAmB,QAarB,SAASC,GAAsBC,EAA4D,CAChG,OAAOC,GACL,MAAOC,GAAoC,CACzC,IAAMC,EAAqB,CACzB,GAAI,IAAI,KAAK,EAAE,YAAY,EAC3B,OAAQH,EAAK,OACb,QAASA,EAAK,aACd,KAAMA,EAAK,KACX,UAAWE,EAAQ,UACnB,OAAQA,EAAQ,OAChB,WAAYA,EAAQ,WACpB,cAAeA,EAAQ,aACzB,EACA,MAAME,GAAcD,CAAG,EACvBE,GAAa,CACX,OAAQL,EAAK,OACb,aAAcA,EAAK,aACnB,KAAMA,EAAK,KACX,UAAWE,EAAQ,UACnB,OAAQA,EAAQ,OAChB,WAAYA,EAAQ,WACpB,cAAeA,EAAQ,aACzB,CAAC,EACD,IAAMI,EAAQ,OAAO,QAAQJ,EAAQ,aAAa,EAAE,OAAO,CAAC,CAAC,CAAEK,CAAC,IAAMA,EAAI,CAAC,EAC3E,GAAID,EAAM,SAAW,EAAG,OACxB,IAAIE,EAAO,MAAMC,EAAa,EAC9B,OAAW,CAACC,EAAMH,CAAC,IAAKD,EAAOE,EAAOG,GAAKH,EAAME,EAAMV,EAAK,OAAQO,CAAC,EACrE,MAAMK,GAAaJ,CAAI,CACzB,EACA,CAACR,EAAK,OAAQA,EAAK,aAAcA,EAAK,IAAI,CAC5C,CACF,CC7BO,SAASa,GAAeC,EAAyB,QAAQ,IAAe,CAC7E,OAAIA,EAAI,gBAAwB,CAAE,qBAAsB,EAAM,EAC1DA,EAAI,YAAoB,CAAE,qBAAsB,EAAM,EACtDA,EAAI,gBAAwB,CAAE,qBAAsB,EAAM,EAE1DA,EAAI,WAAmB,CAAE,qBAAsB,EAAK,EACpDA,EAAI,eAAiB,iBAAyB,CAAE,qBAAsB,EAAK,EAC3EA,EAAI,eAAiB,YAAoB,CAAE,qBAAsB,EAAK,EACtEA,EAAI,eAAiB,UAAkB,CAAE,qBAAsB,EAAK,EACpEA,EAAI,eAAiB,SAAiB,CAAE,qBAAsB,EAAK,EACnEA,EAAI,kBAAoBA,EAAI,cAAsB,CAAE,qBAAsB,EAAK,EAC/EA,EAAI,cAAsB,CAAE,qBAAsB,EAAK,EAEpD,CAAE,qBAAsB,EAAM,CACvC,CC7BA,OAAS,OAAAC,GAAK,QAAAC,GAAM,aAAAC,OAAiB,MAiEjC,OACE,OAAAC,GADF,QAAAC,OAAA,oBAtCJ,IAAMC,GAAY,IAAI,IAEtB,SAASC,GAAQC,EAAqB,CACpC,IAAMC,EAASH,GAAU,IAAIE,CAAG,EAChC,GAAIC,EAAQ,OAAOA,EACnB,IAAM,EAAI,SAASD,EAAI,MAAM,EAAG,CAAC,EAAG,EAAE,EAChCE,EAAI,SAASF,EAAI,MAAM,EAAG,CAAC,EAAG,EAAE,EAChCG,EAAI,SAASH,EAAI,MAAM,EAAG,CAAC,EAAG,EAAE,EAChCI,EAAM,eAAe,CAAC,IAAIF,CAAC,IAAIC,CAAC,IACtC,OAAAL,GAAU,IAAIE,EAAKI,CAAG,EACfA,CACT,CAEA,IAAMC,GAAQ,UAEP,SAASC,GAAY,CAAE,OAAAC,EAAQ,MAAAC,EAAO,MAAAC,EAAQ,GAAO,WAAAC,EAAa,EAAM,EAAU,CACvF,GAAM,CAAE,OAAAC,CAAO,EAAIC,GAAU,EACvBC,EAAOF,GAAQ,SAAW,GAC1BG,EAAQ,CAAC,GAAGP,CAAM,EAClBQ,EAAa,CAAC,GAAGP,CAAK,EAEtBQ,EAAcF,EAAM,OAAS,EAC7BG,EAAY,KAAK,IAAI,EAAG,KAAK,OAAOJ,EAAOG,GAAe,CAAC,CAAC,EAC5DE,EAAY,IAAI,OAAO,KAAK,MAAMD,EAAY,CAAC,CAAC,EAEhDE,EAAOL,EACV,IAAI,CAACM,EAAIC,IAAM,CACd,IAAMC,EAAUD,EAAIN,EAAW,OACzBQ,EAAUb,GAAc,CAACY,EAAU,IAAMA,EAAUP,EAAWM,CAAC,EAAKD,EACpEI,EAAQf,EAAQgB,EAAQ,MAAQH,EAAUG,EAAQ,OAASA,EAAQ,MACzE,MAAO,GAAG1B,GAAQyB,CAAK,CAAC,GAAGD,CAAO,GAAGlB,EAAK,EAC5C,CAAC,EACA,KAAK,EAAE,EAEJqB,EAAU,SAASR,CAAS,GAAGC,CAAI,GACnCQ,EAAa,SAAST,CAAS,GAAGC,CAAI,GAE5C,OACEtB,GAAC+B,GAAA,CAAI,cAAc,SAAS,SAAU,EAAG,MAAM,OAC7C,UAAAhC,GAACiC,GAAA,CAAM,SAAAH,EAAQ,EACf9B,GAACiC,GAAA,CAAM,SAAAF,EAAW,GACpB,CAEJ,CC1C6B,cAAAG,OAAA,oBAhB7B,IAAIC,GAA8B,KAClC,SAASC,IAAoB,CAC3B,OAAKD,KAAYA,GAAaE,GAAe,GACtCF,EACT,CAEO,SAASG,GAAYC,EAAc,CACxC,GAAM,CAAE,IAAAC,CAAI,EAAIC,EAAY,EACtBC,EAAO,QAAQ,QAAQ,SAAW,GAClCC,EAAOP,GAAQ,EAEfQ,EACJJ,EAAI,cAAgB,QACnBA,EAAI,cAAgB,QAAUG,EAAK,qBAChCE,EAAOH,GAAQ,EAAI,CAAC,GAAGH,EAAM,MAAM,EAAE,OAAS,EAEpD,OAAOK,GAAaC,EAAOX,GAACY,GAAA,CAAa,GAAGP,EAAO,EAAKL,GAACa,GAAA,CAAS,GAAGR,EAAO,CAC9E,CC5BA,OAAS,OAAAS,EAAK,QAAAC,EAAM,aAAAC,OAAiB,MAsBjC,OAgDI,YAAAC,GA/CF,OAAAC,EADF,QAAAC,MAAA,oBAlBJ,IAAMC,GAAc,GAEpB,SAASC,GAAQC,EAAoB,CACnC,IAAMC,EAAQ,KAAK,MAAMD,EAAK,GAAI,EAC5BE,EAAI,KAAK,MAAMD,EAAQ,EAAE,EACzBE,EAAIF,EAAQ,GAClB,MAAO,GAAGC,CAAC,IAAI,OAAOC,CAAC,EAAE,SAAS,EAAG,GAAG,CAAC,EAC3C,CAEA,SAASC,IAAuB,CAC9B,GAAM,CAAE,OAAAC,CAAO,EAAIC,GAAU,EACvBC,EAAOF,GAAQ,SAAW,GAChC,OAAO,KAAK,IAAI,GAAIE,EAAOT,EAAW,CACxC,CAEA,SAASU,EAAI,CAAE,KAAAC,EAAM,MAAAC,CAAM,EAA0C,CACnE,IAAMC,EAAYP,GAAa,EAC/B,OACEP,EAACe,EAAA,CACC,UAAAhB,EAACgB,EAAA,CAAI,MAAOD,EAAY,SAAAF,EAAK,EAC7Bb,EAACgB,EAAA,CAAI,MAAOd,GAAa,eAAe,WACrC,SAAAY,EACH,GACF,CAEJ,CAEO,SAASG,GAAcC,EAmB3B,CACD,IAAMC,EAAIC,EAAW,EACfC,EAAS,CAAC,GAAGH,EAAM,MAAM,EACzBI,EAAQ,CAAC,GAAGJ,EAAM,KAAK,EAIvBK,EACJtB,EAACe,EAAA,CACE,UAAAK,EAAO,IAAI,CAACG,EAAIC,IAAM,CACrB,IAAMC,EAAUD,EAAIH,EAAM,OACpBK,EAAUT,EAAM,YAAc,CAACQ,EAAU,IAAMA,EAAUJ,EAAMG,CAAC,EAAKD,EACrEI,EAAQV,EAAM,MAAQW,EAAQ,MAAQH,EAAUG,EAAQ,OAASA,EAAQ,MAC/E,OACE7B,EAAC8B,EAAA,CAAa,KAAI,GAAC,MAAOF,EACvB,SAAAD,GADQF,CAEX,CAEJ,CAAC,EACAP,EAAM,UACLjB,EAAAF,GAAA,CACE,UAAAC,EAAC8B,EAAA,CAAK,cAAE,EACR9B,EAAC8B,EAAA,CAAK,OAAM,GAAC,SAAQ,GAAC,MAAOD,EAAQ,MAClC,SAAAX,EAAM,SACT,GACF,GAEJ,EAIIa,EAAkBb,EAAM,YAAY,OAAS,EACjDlB,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,QAAU,SAAAX,EAAM,YAAY,CAAC,EAAG,EAErDlB,EAAC8B,EAAA,CAAK,aAAC,EAGHE,EAAOd,EAAM,KACbe,EAAS,OAAO,UAAUD,EAAK,MAAM,EAAI,GAAGA,EAAK,MAAM,GAAKA,EAAK,OAAO,QAAQ,CAAC,EAOjFE,EAAY,CAAChB,EAAM,YAAcA,EAAM,eAAiB,KACxDiB,EAASjB,EAAM,WACnBlB,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,QAAU,SAAAV,EAAE,SAAS,gBAAgB,EACxDe,EACFlC,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,QAAU,SAAAV,EAAE,SAAS,kBAAkB,EAC1Da,EAAK,QACPhC,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,MAAQ,YAAGG,EAAK,QAAQ,SAAMA,EAAK,YAAY,GAAG,EAEvEhC,EAAC8B,EAAA,CAAK,aAAC,EAEHM,EAASlB,EAAM,YAAcgB,EACjClC,EAAC8B,EAAA,CAAK,aAAC,EACLE,EAAK,QACPhC,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,MAAQ,YAAGG,EAAK,SAAS,IAAIA,EAAK,KAAK,SAAMA,EAAK,GAAG,YAASC,CAAM,IAAI,EAE7FjC,EAAC8B,EAAA,CAAK,aAAC,EAEHO,EAASnB,EAAM,YAAcgB,EACjClC,EAAC8B,EAAA,CAAK,aAAC,EACLE,EAAK,QACPhC,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,MAAQ,SAAA1B,GAAQ6B,EAAK,SAAS,EAAE,EAErDhC,EAAC8B,EAAA,CAAK,aAAC,EAMT,OACE7B,EAACe,EAAA,CAAI,cAAc,SACjB,UAAAhB,EAACY,EAAA,CAAI,KAAMW,EAAU,MAAOY,EAAQ,EACpCnC,EAACY,EAAA,CAAI,KAAMZ,EAAC8B,EAAA,CAAK,aAAC,EAAS,MAAOM,EAAQ,EAC1CpC,EAACY,EAAA,CAAI,KAAMmB,EAAiB,MAAOM,EAAQ,GAC7C,CAEJ,CAEO,SAASC,IAAgB,CAC9B,IAAM,EAAIlB,EAAW,EACrB,OACEnB,EAACe,EAAA,CAAI,cAAc,SACjB,UAAAhB,EAACY,EAAA,CACC,KAAMZ,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,QAAU,WAAE,QAAQ,OAAO,EACtD,MAAO7B,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,MAAQ,WAAE,QAAQ,gBAAgB,EAChE,EACA7B,EAACY,EAAA,CAAI,KAAMZ,EAAC8B,EAAA,CAAK,aAAC,EAAS,MAAO7B,EAAC6B,EAAA,CAAK,MAAOD,EAAQ,MAAO,iBAAK,EAAE,OAAO,MAAK,EAAS,EAC1F7B,EAACY,EAAA,CAAI,KAAMZ,EAAC8B,EAAA,CAAK,aAAC,EAAS,MAAO9B,EAAC8B,EAAA,CAAK,aAAC,EAAS,GACpD,CAEJ,CAEO,SAASS,GAAerB,EAM5B,CACD,IAAMC,EAAIC,EAAW,EACfa,EAAS,OAAO,UAAUf,EAAM,MAAM,EAAI,GAAGA,EAAM,MAAM,GAAKA,EAAM,OAAO,QAAQ,CAAC,EACpFsB,EAAO,GAAGrB,EAAE,QAAQ,WAAW,SAAMD,EAAM,SAAS,UAAOA,EAAM,GAAG,YAASe,CAAM,UAAO9B,GAAQe,EAAM,UAAU,CAAC,GACzH,OACEjB,EAACe,EAAA,CAAI,cAAc,SACjB,UAAAhB,EAACY,EAAA,CACC,KAAMZ,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,QAAU,SAAAW,EAAK,EAC1C,MAAOxC,EAAC8B,EAAA,CAAK,MAAOD,EAAQ,MAAQ,SAAAV,EAAE,QAAQ,cAAc,EAC9D,EACAnB,EAACY,EAAA,CAAI,KAAMZ,EAAC8B,EAAA,CAAK,aAAC,EAAS,MAAO7B,EAAC6B,EAAA,CAAK,MAAOD,EAAQ,MAAO,iBAAKV,EAAE,OAAO,MAAK,EAAS,EAC1FnB,EAACY,EAAA,CAAI,KAAMZ,EAAC8B,EAAA,CAAK,aAAC,EAAS,MAAO9B,EAAC8B,EAAA,CAAK,aAAC,EAAS,GACpD,CAEJ,CXrFW,cAAAW,EAmWL,QAAAC,MAnWK,oBArDJ,SAASC,GAAe,CAAE,OAAAC,CAAO,EAA+B,CACrE,GAAM,CAAE,OAAAC,EAAQ,aAAAC,EAAc,KAAAC,CAAK,EAAIH,EACjC,CAAE,IAAAI,CAAI,EAAIC,EAAY,EACtBC,EAAIC,EAAW,EAEf,CAACC,EAAOC,CAAQ,EAAIC,EAAgB,SAAS,EAC7C,CAACC,EAAQC,CAAS,EAAIF,EAAwB,IAAI,EAClD,CAACG,EAAUC,CAAW,EAAIJ,EAAwB,IAAI,EA6C5D,OA3CAK,EAAU,IAAM,CACd,IAAIC,EAAY,GAChB,OAAAP,EAAS,SAAS,EAClBG,EAAU,IAAI,EACdE,EAAY,IAAI,GACf,SAAY,CACX,GAAI,CACF,IAAMG,EAAQ,MAAMC,GAAiBjB,CAAM,EAC3C,GAAIe,EAAW,OACf,GAAIb,IAAS,SAAU,CACrB,IAAMgB,EAAO,MAAMC,EAAa,EAChC,GAAIJ,EAAW,OACf,IAAMK,EAAcJ,EAAM,OAAQK,GAAMH,EAAKG,EAAE,IAAI,GAAG,KAAK,EAAE,MAAM,EAAGlB,EAAI,WAAW,EACrF,GAAIiB,EAAY,SAAW,EAAG,CAC5BP,EAAYR,EAAE,SAAS,OAAO,UAAU,EACxCG,EAAS,OAAO,EAChB,MACF,CACAG,EAAU,CAAE,SAAUS,EAAa,cAAe,CAAE,CAAC,EACrDZ,EAAS,QAAQ,EACjB,MACF,CACA,IAAMc,EAAWC,GAAcP,EAAOb,EAAI,WAAW,EACrD,GAAImB,EAAS,SAAW,EAAG,CACzBT,EAAYR,EAAE,SAAS,OAAO,UAAUL,CAAM,CAAC,EAC/CQ,EAAS,OAAO,EAChB,MACF,CACA,IAAMgB,EAAM,KAAK,IAAI,EAAG,KAAK,IAAIF,EAAS,OAAS,EAAGrB,CAAY,CAAC,EAC7DwB,EAAWC,GAAcJ,EAASE,CAAG,EAAItB,CAAI,EACnDS,EAAU,CAAE,SAAAc,EAAU,cAAeH,EAAS,MAAO,CAAC,EACtDd,EAAS,QAAQ,CACnB,OAASmB,EAAK,CACZ,GAAIZ,EAAW,OACfF,EAAac,EAAc,OAAO,EAClCnB,EAAS,OAAO,CAClB,CACF,GAAG,EACI,IAAM,CACXO,EAAY,EACd,CACF,EAAG,CAACf,EAAQC,EAAcC,EAAMC,EAAI,YAAaE,CAAC,CAAC,EAE/CE,IAAU,UACLX,EAACgC,GAAA,CAAS,KAAMvB,EAAE,SAAS,QAAS,MAAOwB,EAAQ,MAAO,EAE/DtB,IAAU,QACLX,EAACkC,GAAA,CAAU,IAAKlB,GAAYP,EAAE,SAAS,OAAO,QAAS,EAE3DK,EAGHd,EAACmC,GAAA,CAEC,OAAQhC,EACR,OAAQW,EACR,MAAOH,EACP,SAAUC,GAJL,GAAGR,CAAM,IAAIC,CAAY,IAAIC,CAAI,IAAIH,EAAO,QAAU,IAAM,GAAG,EAKtE,EATkB,IAWtB,CAEA,SAASgC,GAAe,CACtB,OAAAhC,EACA,OAAAW,EACA,MAAAH,EACA,SAAAC,CACF,EAKG,CACD,GAAM,CAAE,OAAAR,EAAQ,aAAAC,EAAc,KAAAC,CAAK,EAAIH,EACjCiC,EAAUjC,EAAO,UAAY,GAC7B,CAAE,IAAAI,CAAI,EAAIC,EAAY,EACtB6B,EAAMC,EAAO,EACb,CAAE,KAAAC,CAAK,EAAIC,GAAO,EAClBC,EAAS,IAAOJ,EAAI,MAAM,OAAS,EAAIA,EAAI,KAAK,EAAIE,EAAK,EACzDG,EAAUC,GAAsB,CAAE,OAAAvC,EAAQ,aAAAC,EAAc,KAAAC,CAAK,CAAC,EAC9DsC,EAAWC,GAAYzC,CAAM,EAE7B0C,EAAQC,GAAS,CACrB,QAAS,CAACX,GAAW7B,EAAI,OAAO,OAChC,OAAQA,EAAI,OACZ,sBAAuB,CAAC6B,GAAW7B,EAAI,sBACvC,kBAAmBA,EAAI,OAAO,kBAC9B,oBAAqBA,EAAI,OAAO,mBAClC,CAAC,EACKyC,EAAcC,GAAe,EAG7BC,EAAY3C,EAAI,OAAO,OAASyC,EAAY,QAAU,KAEtDG,EAAcC,GAAO,EAAK,EAC1BC,EAAmBD,GAAe,CAAC,EACnCE,EAAeF,GAAe,EAAE,EAChC,CAACG,GAAaC,EAAc,EAAI3C,EAAS,EAAK,EAC9C,CAAC4C,GAAaC,EAAc,EAAI7C,EAAwB,IAAI,EAC5D,CAAC8C,GAAYC,EAAa,EAAI/C,EAAS,EAAK,EAElDK,EAAU,IAAM,CACd,GAAIuC,KAAgB,KAAM,OAC1B,IAAMI,EAAK,WAAW,IAAML,GAAe,EAAK,EAAG,GAAI,EACvD,MAAO,IAAM,aAAaK,CAAE,CAC9B,EAAG,CAACJ,EAAW,CAAC,EAEhB,GAAM,CAAE,QAAAK,EAAS,WAAAC,EAAY,UAAAC,EAAW,KAAAC,EAAK,EAAIC,GAAY,CAC3D,SAAUpD,EAAO,SACjB,QAASH,IAAU,SACnB,WAAawD,GAAM,CACbhB,EAAY,UAChBA,EAAY,QAAU,GACtBvC,EAAS,SAAS,EAClB,QAAQ,QAAQ8B,EAAQ0B,EAAeD,CAAC,CAAC,CAAC,EAAE,MAAOpC,GAAQ,CACzD,QAAQ,MAAM,6BAA8BA,CAAG,CACjD,CAAC,EACH,EACA,SAAU,IAAMnB,EAASD,IAAU,SAAW,SAAW,QAAQ,EACjE,MAAOyB,EACH,OACA,IAAM,CACJ,IAAMiC,EAAMP,EAAQ,QAAUhD,EAAO,SAASgD,EAAQ,QAAQ,SAAS,EAAI,OACvEO,GAAUvB,EAAM,UAAUuB,EAAI,IAAI,CACxC,EACJ,WAAY,IAAMT,GAAc,EAAI,EACpC,aAAc,IAAMA,GAAc,EAAK,CACzC,CAAC,EAED1C,EAAU,IAAM,CACVkB,GACA4B,IAAcX,EAAiB,UACnCA,EAAiB,QAAUW,EACvBD,IAAe,OACfA,IAAe,SAAWxD,EAAI,OAAO,UAAUuC,EAAM,MAAM,EAC3DiB,IAAe,YAAcxD,EAAI,OAAO,WAAWuC,EAAM,UAAU,EACnEiB,IAAe,YACbxD,EAAI,OAAO,UAAUuC,EAAM,QAAQ,EACnCvC,EAAI,OAAO,WAAWuC,EAAM,UAAU,IAE9C,EAAG,CAACV,EAAS4B,EAAWD,EAAYjB,EAAOvC,EAAI,OAAO,SAAUA,EAAI,OAAO,SAAS,CAAC,EAErFW,EAAU,IAAM,CACd,GAAIkB,EAAS,OACb,IAAMR,EAAMkC,EAAQ,SAAS,WAAa,GAE1C,GADIlC,IAAQ,IACRA,IAAQ0B,EAAa,QAAS,OAClCA,EAAa,QAAU1B,EACvB,IAAMyC,EAAMvD,EAAO,SAASc,CAAG,EACzB0C,EAAOxD,EAAO,SAASc,EAAM,CAAC,EAChCyC,GAAO9D,EAAI,uBAAuBuC,EAAM,UAAUuB,EAAI,IAAI,EAC1DC,GAAMxB,EAAM,SAASwB,EAAK,IAAI,CACpC,EAAG,CAAClC,EAAS0B,EAAQ,SAAS,UAAWhB,EAAOvC,EAAI,sBAAuBO,EAAO,QAAQ,CAAC,EAI3FyD,EACE,CAACC,EAAQC,IAAQ,CAKf,GAAIA,EAAI,IAAK,CACXjB,GAAe,EAAI,EACnBE,GAAe,KAAK,IAAI,CAAC,EACzB,MACF,CACF,EACA,CAAE,SAAUtB,GAAWzB,IAAU,QAAS,CAC5C,EAEA4D,EACE,CAACC,EAAQC,IAAQ,CACf,GAAIA,EAAI,OAAQ,CACd7D,EAAS,QAAQ,EACjB,MACF,CACA,GAAI6D,EAAI,OAAQ,CACdhC,EAAO,EACP,MACF,CACF,EACA,CAAE,SAAU9B,IAAU,QAAS,CACjC,EAKA4D,EACE,CAACG,EAAOD,IAAQ,CACVA,EAAI,MAAQC,IAAU,MACxBC,GAAc,EAAI,EAClBpC,EAAK,EAET,EACA,CAAE,SAAUH,GAAWzB,IAAU,QAAS,CAC5C,EAEA4D,EACE,CAACG,EAAOD,IAAQ,CACd,GAAIA,EAAI,OAAQ,CACdhC,EAAO,EACP,MACF,CACA,GAAIgC,EAAI,OAAQ,CACd,IAAMG,EAAUvE,EAAe,EAC3BC,IAAS,OACX+B,EAAI,QAAQ,CACV,KAAM,WACN,OAAQ,CAAE,OAAAjC,EAAQ,aAAAC,EAAc,KAAAC,EAAM,QAASH,EAAO,OAAQ,CAChE,CAAC,EACQG,IAAS,UAAYsE,GAAW9D,EAAO,cAChD2B,EAAO,EAEPJ,EAAI,QAAQ,CACV,KAAM,WACN,OAAQ,CAAE,OAAAjC,EAAQ,aAAcwE,EAAS,KAAAtE,EAAM,QAASH,EAAO,OAAQ,CACzE,CAAC,EAEH,MACF,CACA,GAAIuE,IAAU,IAAK,CACjBrC,EAAI,QAAQ,CACV,KAAM,WACN,OAAQ,CAAE,OAAAjC,EAAQ,aAAc,EAAG,KAAM,SAAU,QAASD,EAAO,OAAQ,CAC7E,CAAC,EACD,MACF,CACF,EACA,CAAE,SAAUQ,IAAU,SAAU,CAClC,EAEA,IAAMkE,EAAYf,EAAQ,QAAQ,OAC5BgB,GAAShB,EAAQ,QAAQ,OAAO,CAACiB,EAAGC,IAAMD,EAAIC,EAAE,OAAQ,CAAC,EACzDC,EAAY,KAAK,IAAI,EAAInB,EAAQ,UACjCoB,GAAUD,EAAY,IACtBE,GAAMD,GAAU,EAAI,KAAK,MAAOL,EAAYK,GAAW,EAAE,EAAI,GAAK,EAElEE,EAAUzE,IAAU,UAAYyD,EAAeN,CAAO,EAAI,KAEhE,GAAI1B,EAAS,CACX,GAAIzB,IAAU,SAAU,OAAOX,EAACqF,GAAA,EAAc,EAC9C,GAAI1E,IAAU,WAAayE,EAAS,CAClC,IAAME,EAAWF,EAAQ,WAAa,IAChCG,GAAOD,EAAW,EAAI,KAAK,MAAOF,EAAQ,UAAYE,EAAY,EAAE,EAAI,GAAK,EAC7EE,GAAY,OAAO,KAAKJ,EAAQ,aAAa,EAAE,OAC/CK,GACJL,EAAQ,YAAc,EAAI,EAAI,KAAK,IAAI,GAAIA,EAAQ,UAAYI,IAAaJ,EAAQ,SAAS,EACzFM,GAAU,KAAK,MAAMD,GAAO,GAAI,EAAI,GAC1C,OACEzF,EAAC2F,GAAA,CACC,UAAWP,EAAQ,UACnB,OAAQA,EAAQ,OAChB,WAAYA,EAAQ,WACpB,IAAKG,GACL,OAAQG,GACV,CAEJ,CACA,IAAME,EAAc9B,EAAQ,QACxBhD,EAAO,SAASgD,EAAQ,QAAQ,SAAS,EACzChD,EAAO,SAASA,EAAO,SAAS,OAAS,CAAC,EACxC+E,EAAa/B,EAAQ,SAAS,OAAS,CAAE,OAAQ,GAAI,MAAO,GAAI,eAAgB,CAAE,EAClFgC,EAAW,IAAI,IACnBhC,EAAQ,QAAQ,OAAQkB,GAAMA,EAAE,OAAS,CAAC,EAAE,IAAKA,GAAMA,EAAE,IAAI,CAC/D,EAAE,KACIe,GACJlB,IAAc,EAAI,EAAI,KAAK,IAAI,GAAIA,EAAYiB,GAAYjB,CAAS,EAChEmB,GAAS,KAAK,MAAMD,GAAU,GAAI,EAAI,GACtCE,GACJ3F,IAAS,SACL,SACA,MAAMD,EAAe,CAAC,IAAIS,EAAO,aAAa,GACpD,OACEd,EAACkG,GAAA,CACC,OAAQN,GAAa,MAAQ,GAC7B,MAAOC,EAAW,MAClB,WAAYvF,IAAS,YACrB,SAAU6F,GAAaP,EAAarF,EAAI,MAAM,EAC9C,YAAaqF,GAAa,OAAS,CAAC,EACpC,MAAO7B,IAAe,QACtB,WAAYJ,GACZ,aAAcT,EACd,KAAM,CACJ,QAASK,GACT,SAAU6C,EAAaxD,EAAU,EAAE,EACnC,aAAAqD,GACA,UAAApB,EACA,MAAO/D,EAAO,SAAS,OACvB,IAAAqE,GACA,OAAAa,GACA,UAAAf,CACF,EACF,CAEJ,CAEA,GAAItE,IAAU,SACZ,OACEX,EAACqG,GAAA,CACC,SAAUzD,EACV,aAAcvC,EACd,cAAeS,EAAO,cACtB,KAAMR,EACN,UAAWuE,EACX,MAAO/D,EAAO,SAAS,OACzB,EAIJ,GAAIH,IAAU,WAAayE,EACzB,OACEpF,EAACsG,GAAA,CACC,SAAU1D,EACV,aAAcvC,EACd,cAAeS,EAAO,cACtB,KAAMR,EACN,QAAS8E,EACX,EAIJ,IAAMQ,EAAc9B,EAAQ,QACxBhD,EAAO,SAASgD,EAAQ,QAAQ,SAAS,EACzChD,EAAO,SAASA,EAAO,SAAS,OAAS,CAAC,EACxC+E,GAAa/B,EAAQ,SAAS,OAAS,CAAE,OAAQ,GAAI,MAAO,GAAI,eAAgB,CAAE,EAExF,OACE9D,EAACuG,GAAA,CACC,SAAU3D,EACV,aAAcvC,EACd,cAAeS,EAAO,cACtB,KAAMR,EACN,OAAQC,EAAI,OACZ,UAAWsE,EACX,MAAO/D,EAAO,SAAS,OACvB,OAAQgE,GACR,IAAKK,GACL,UAAWF,EACX,OAAQW,GAAa,MAAQ,GAC7B,MAAOC,GAAW,MAClB,WAAY9B,IAAe,QAC3B,WAAYzD,IAAS,YACrB,SAAU6F,GAAaP,EAAarF,EAAI,MAAM,EAC9C,YAAaqF,GAAa,OAAS,CAAC,EACpC,WAAYjC,GACZ,aAAcT,EAChB,CAEJ,CAEA,SAASiD,GAAaK,EAAwBC,EAAoC,CAChF,OAAKD,GACKC,IAAW,KAAOD,EAAK,QAAUA,EAAK,UACpC,KAFM,IAGpB,CAEA,SAASE,GAAQC,EAAoB,CACnC,IAAMC,EAAQ,KAAK,MAAMD,EAAK,GAAI,EAC5BE,EAAI,KAAK,MAAMD,EAAQ,EAAE,EACzBzC,EAAIyC,EAAQ,GAClB,MAAO,GAAGC,CAAC,IAAI,OAAO1C,CAAC,EAAE,SAAS,EAAG,GAAG,CAAC,EAC3C,CAEA,SAASoC,GAAaO,EAmBnB,CACD,IAAMrG,EAAIC,EAAW,EACfqG,EAAeD,EAAM,QAAU,EAAI,EAAIA,EAAM,UAAYA,EAAM,MACrE,OACE7G,EAAC+G,EAAA,CAAI,cAAc,SAAS,SAAU,EAAG,SAAU,EAAG,MAAM,OAAO,OAAO,OACxE,UAAAhH,EAACiH,GAAA,CACC,SAAUH,EAAM,SAChB,aAAcA,EAAM,aACpB,cAAeA,EAAM,cACrB,KAAMA,EAAM,KACZ,OAAQA,EAAM,OACd,UAAWA,EAAM,UACjB,MAAOA,EAAM,MACb,UAAWA,EAAM,UACnB,EAEA7G,EAAC+G,EAAA,CAAI,SAAU,EAAG,cAAc,SAAS,WAAW,SAAS,eAAe,SAC1E,UAAAhH,EAACkH,GAAA,CACC,OAAQJ,EAAM,OACd,MAAOA,EAAM,MACb,MAAOA,EAAM,WACb,WAAYA,EAAM,WACpB,EAECA,EAAM,UACL9G,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,OAAM,GAAC,SAAQ,GAAC,MAAOlF,EAAQ,MAClC,SAAA6E,EAAM,SACT,EACF,EAGDA,EAAM,YAAY,OAAS,GAC1B9G,EAACgH,EAAA,CAAI,UAAW,EAAG,cAAc,SAAS,WAAW,SAClD,SAAAF,EAAM,YAAY,MAAM,EAAG,CAAC,EAAE,IAAI,CAACM,EAAIC,IACtCrH,EAACmH,EAAA,CAAa,MAAOlF,EAAQ,QAC1B,SAAAmF,GADQC,CAEX,CACD,EACH,EAGDP,EAAM,YACL9G,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,QAAU,SAAAxB,EAAE,SAAS,WAAW,EACvD,EAGD,CAACqG,EAAM,YAAcA,EAAM,cAC1B9G,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,QAAU,SAAA6E,EAAM,aAAa,EACpD,GAEJ,EAEA7G,EAAC+G,EAAA,CAAI,cAAc,SACjB,UAAAhH,EAACsH,GAAA,CAAY,KAAMP,EAAc,EACjC/G,EAACgH,EAAA,CAAI,eAAe,SAAS,UAAW,EACtC,SAAA/G,EAACkH,EAAA,CAAK,MAAOlF,EAAQ,MAClB,UAAA6E,EAAM,UAAU,IAAEA,EAAM,MAAM,WAAMJ,GAAQI,EAAM,SAAS,EAAE,WAAMA,EAAM,IAAI,IAAErG,EAAE,SAAS,UAAU,IAAI,WAAMqG,EAAM,OAAO,IAAErG,EAAE,SAAS,UAAU,QACrJ,EACF,EACAT,EAACgH,EAAA,CAAI,eAAe,SAAS,UAAW,EACtC,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAxB,EAAE,SAAS,QAAQ,OAAO,EACzD,GACF,GACF,CAEJ,CAEA,SAASwG,GAAUH,EAShB,CACD,IAAMrG,EAAIC,EAAW,EACf6G,EAAW9G,EAAE,SAAS,MAAMqG,EAAM,IAAI,EACtCU,EAAa/G,EAAE,SAAS,QAAQqG,EAAM,MAAM,EAC5CW,EAAOrB,EAAaU,EAAM,SAAU,EAAE,EACtCY,EACJZ,EAAM,OAAS,SACX,GAAGW,CAAI,WAAQhH,EAAE,SAAS,WAAW,WAAQ+G,CAAU,GACvD,GAAGC,CAAI,WAAQhH,EAAE,SAAS,aAAaqG,EAAM,aAAe,EAAGA,EAAM,aAAa,CAAC,WAAQS,CAAQ,WAAQC,CAAU,GACrHG,EAAQ,GAAGb,EAAM,SAAS,IAAIA,EAAM,KAAK,WAAQJ,GAAQI,EAAM,SAAS,CAAC,GAC/E,OACE7G,EAAC+G,EAAA,CACC,UAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAyF,EAAK,EAClC1H,EAACgH,EAAA,CAAI,SAAU,EAAG,EAClBhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAA0F,EAAM,GACrC,CAEJ,CAEA,SAASL,GAAY,CAAE,KAAAM,CAAK,EAAqB,CAC/C,IAAMC,EAAO,QAAQ,OAAO,SAAW,GACjCC,EAAQ,KAAK,IAAI,GAAI,KAAK,IAAI,GAAID,EAAO,EAAE,CAAC,EAC5CE,EAAS,KAAK,MAAMD,EAAQ,KAAK,IAAI,EAAG,KAAK,IAAI,EAAGF,CAAI,CAAC,CAAC,EAC1DI,EAAQF,EAAQC,EACtB,OACE9H,EAAC+G,EAAA,CAAI,eAAe,SAClB,UAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,OAAS,kBAAI,OAAO8F,CAAM,EAAE,EACjD/H,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,kBAAI,OAAO+F,CAAK,EAAE,GACjD,CAEJ,CAEA,SAAS3B,GAAWS,EAOjB,CACD,IAAMrG,EAAIC,EAAW,EACfkH,EAAOd,EAAM,QAAU,EAAI,EAAIA,EAAM,UAAYA,EAAM,MACvDmB,EACJnB,EAAM,OAAS,SACX,GAAGV,EAAaU,EAAM,SAAU,EAAE,CAAC,WAAQrG,EAAE,SAAS,WAAW,GACjE,GAAG2F,EAAaU,EAAM,SAAU,EAAE,CAAC,WAAQrG,EAAE,SAAS,MAAM,QAAQqG,EAAM,aAAe,EAAGA,EAAM,aAAa,CAAC,GACtH,OACE7G,EAAC+G,EAAA,CAAI,cAAc,SAAS,WAAW,SAAS,eAAe,SAAS,MAAM,OAAO,OAAO,OAC1F,UAAAhH,EAACmH,EAAA,CAAK,KAAI,GAAC,MAAOlF,EAAQ,QACvB,SAAAxB,EAAE,SAAS,MAAM,MACpB,EACAT,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAgG,EAAS,EACxC,EACAjI,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACsH,GAAA,CAAY,KAAMM,EAAM,EAC3B,EACA5H,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAxB,EAAE,SAAS,MAAM,SAASqG,EAAM,UAAWA,EAAM,KAAK,EAAE,EACvF,EACA9G,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAxB,EAAE,SAAS,MAAM,KAAK,EACrD,GACF,CAEJ,CAEA,SAASyB,GAAU,CAAE,IAAAgG,CAAI,EAAoB,CAC3C,IAAMzH,EAAIC,EAAW,EACrB,OACET,EAAC+G,EAAA,CAAI,cAAc,SAAS,WAAW,SAAS,eAAe,SAAS,MAAM,OAAO,OAAO,OAC1F,UAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAiG,EAAI,EACjClI,EAACgH,EAAA,CAAI,UAAW,EACd,SAAA/G,EAACkH,EAAA,CAAK,MAAOlF,EAAQ,MAAO,iBAAKxB,EAAE,OAAO,MAAK,EACjD,EACAT,EAACmI,GAAA,EAAQ,GACX,CAEJ,CAEA,SAASA,IAAU,CACjB,IAAM9F,EAAMC,EAAO,EACnB,OAAAiC,EAAS,CAACC,EAAQC,IAAQ,CACpBA,EAAI,QAAQpC,EAAI,KAAK,CAC3B,CAAC,EACM,IACT,CAEA,SAASL,GAAS,CAAE,KAAAoG,EAAM,MAAAC,CAAM,EAAoC,CAClE,OACErI,EAACgH,EAAA,CAAI,WAAW,SAAS,eAAe,SAAS,MAAM,OAAO,OAAO,OACnE,SAAAhH,EAACmH,EAAA,CAAK,MAAOkB,EAAQ,SAAAD,EAAK,EAC5B,CAEJ,CAEA,SAAS9B,GAAYQ,EAMlB,CACD,GAAM,CAAE,QAAA1B,CAAQ,EAAI0B,EACd5B,EAAUE,EAAQ,WAAa,IAC/BD,EAAMD,EAAU,EAAI,KAAK,MAAOE,EAAQ,UAAYF,EAAW,EAAE,EAAI,GAAK,EAC1EoD,EAAa,OAAO,KAAKlD,EAAQ,aAAa,EAAE,OAChDmD,EAAMnD,EAAQ,YAAc,EAAI,EAAI,KAAK,IAAI,GAAIA,EAAQ,UAAYkD,GAAclD,EAAQ,SAAS,EACpGY,EAAS,KAAK,MAAMuC,EAAM,GAAI,EAAI,GAElC9H,EAAIC,EAAW,EACf6G,EAAW9G,EAAE,SAAS,MAAMqG,EAAM,IAAI,EACtCW,EAAOrB,EAAaU,EAAM,SAAU,EAAE,EACtCmB,EACJnB,EAAM,OAAS,SACX,GAAGW,CAAI,WAAQhH,EAAE,SAAS,WAAW,GACrC,GAAGgH,CAAI,WAAQhH,EAAE,SAAS,aAAaqG,EAAM,aAAe,EAAGA,EAAM,aAAa,CAAC,WAAQS,CAAQ,GAQnGiB,EAAS,SALb1B,EAAM,OAAS,OACXrG,EAAE,SAAS,QAAQ,UACnBqG,EAAM,OAAS,UAAYA,EAAM,aAAe,GAAKA,EAAM,cACzDrG,EAAE,SAAS,QAAQ,SACnBA,EAAE,SAAS,QAAQ,WACM,aAAUA,EAAE,SAAS,QAAQ,cAAc,eAAYA,EAAE,SAAS,QAAQ,QAAQ,GAEnH,OACER,EAAC+G,EAAA,CAAI,cAAc,SAAS,WAAW,SAAS,eAAe,SAAS,SAAU,EAAG,MAAM,OAAO,OAAO,OACvG,UAAAhH,EAACmH,EAAA,CAAK,KAAI,GAAC,MAAOlF,EAAQ,QACvB,SAAAxB,EAAE,SAAS,gBACd,EACAT,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAgG,EAAS,EACxC,EAEAhI,EAAC+G,EAAA,CAAI,UAAW,EAAG,cAAc,MAAM,eAAe,SACpD,UAAAhH,EAACyI,EAAA,CAAS,MAAOhI,EAAE,SAAS,UAAU,MAAO,MAAO,OAAO2E,EAAQ,SAAS,EAAG,MAAOnD,EAAQ,KAAM,EACpGjC,EAACyI,EAAA,CACC,MAAOhI,EAAE,SAAS,UAAU,OAC5B,MAAO,OAAO2E,EAAQ,MAAM,EAC5B,MAAOA,EAAQ,OAAS,EAAInD,EAAQ,MAAQA,EAAQ,MACtD,EACAjC,EAACyI,EAAA,CAAS,MAAOhI,EAAE,SAAS,UAAU,IAAK,MAAO,OAAO0E,CAAG,EAAG,MAAOlD,EAAQ,OAAQ,EACtFjC,EAACyI,EAAA,CAAS,MAAOhI,EAAE,SAAS,UAAU,SAAU,MAAO,GAAGuF,CAAM,IAAK,MAAO/D,EAAQ,OAAQ,GAC9F,EAEAjC,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAxB,EAAE,SAAS,UAAU,QAAQiG,GAAQtB,EAAQ,UAAU,CAAC,EAAE,EACzF,EAEApF,EAACgH,EAAA,CAAI,SAAU,EAAG,EAElBhH,EAACgH,EAAA,CAAI,UAAW,EACd,SAAAhH,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAuG,EAAO,EACtC,GACF,CAEJ,CAEA,SAASC,EAAS,CAAE,MAAAC,EAAO,MAAAC,EAAO,MAAAN,CAAM,EAAoD,CAC1F,OACEpI,EAAC+G,EAAA,CAAI,cAAc,SAAS,WAAW,SAAS,QAAS,EACvD,UAAAhH,EAACmH,EAAA,CAAK,KAAI,GAAC,MAAOkB,EAAQ,SAAAM,EAAM,EAChC3I,EAACmH,EAAA,CAAK,MAAOlF,EAAQ,MAAQ,SAAAyG,EAAM,GACrC,CAEJ","names":["useState","useEffect","useRef","Box","Text","useApp","useInput","shuffle","arr","rng","out","i","j","tmp","mulberry32","seed","t","chunkChapters","words","chapterSize","chunks","i","buildPlaylist","chapter","mode","seed","rng","mulberry32","shuffle","initialState","target","reduce","state","ev","candidate","targetUpToCandidate","startSession","playlist","now","initialState","feedSession","session","ev","state","effect","reduce","finished","nextIndex","results","skipSession","result","sessionSummary","errors","a","r","durationMs","perWordErrors","useEffect","useReducer","useRef","useState","useInput","useApp","reducer","state","action","startSession","skipSession","r","feedSession","session","lastEffect","c","classifyInputBatch","input","chars","cleaned","cp","useWordLoop","playlist","onComplete","onTab","onEscape","onSkip","onImeBlock","onValidInput","enabled","dispatch","useReducer","completedRef","useRef","tick","setTick","useState","exit","useApp","useInput","key","kind","useEffect","id","t","useEffect","useRef","useAudio","opts","initedRef","useRef","useEffect","initAudio","playKeystroke","playCorrect","playWrong","word","playPronunciation","prefetchPronunciation","useCallback","useSessionPersistence","meta","useCallback","summary","rec","appendSession","addChapter","dirty","n","book","loadMistakes","word","bump","saveMistakes","detectTermCaps","env","Box","Text","useStdout","jsx","jsxs","SGR_CACHE","sgrBold","hex","cached","g","b","sgr","RESET","BigWordHuge","target","typed","error","hideTarget","stdout","useStdout","cols","chars","typedChars","visualWidth","visualPad","actualPad","body","ch","i","isTyped","display","color","PALETTE","topLine","bottomLine","Box","Text","jsx","cachedCaps","getCaps","detectTermCaps","BigWordAuto","props","cfg","useAppState","cols","caps","wantsHuge","fits","BigWordHuge","BigWord","Box","Text","useStdout","Fragment","jsx","jsxs","RIGHT_WIDTH","fmtTime","ms","total","m","s","useLeftWidth","stdout","useStdout","cols","Row","left","right","leftWidth","Box","StealthTyping","props","t","useStrings","target","typed","wordCell","ch","i","isTyped","display","color","PALETTE","Text","translationCell","info","accFmt","showAudio","right1","right2","right3","StealthPaused","StealthSummary","line","jsx","jsxs","PracticeScreen","params","dictId","chapterIndex","mode","cfg","useAppState","t","useStrings","phase","setPhase","useState","loaded","setLoaded","errorMsg","setErrorMsg","useEffect","cancelled","words","ensureDictionary","book","loadMistakes","reviewWords","w","chapters","chunkChapters","idx","playlist","buildPlaylist","err","Centered","PALETTE","ErrorView","PracticeRunner","stealth","nav","useNav","exit","useApp","goBack","persist","useSessionPersistence","dictName","useDictName","audio","useAudio","audioStatus","useAudioStatus","audioWarn","finishedRef","useRef","lastEffectSeqRef","lastIndexRef","infoVisible","setInfoVisible","infoShownAt","setInfoShownAt","imeBlocked","setImeBlocked","id","session","lastEffect","effectSeq","tick","useWordLoop","s","sessionSummary","cur","next","useInput","_input","key","input","setSilentExit","nextIdx","completed","errors","a","r","elapsedMs","minutes","wpm","summary","StealthPaused","sMinutes","sWpm","sErrWords","sAcc","sAccPct","StealthSummary","currentWord","inputState","errWords","accFrac","accPct","chapterLabel","StealthTyping","pickPhonetic","truncateName","PausedView","SummaryView","TypingLayout","word","accent","fmtTime","ms","total","m","props","progressFrac","Box","StatusBar","BigWordAuto","Text","tr","i","ProgressBar","modeName","accentName","name","left","right","frac","cols","width","filled","empty","subtitle","msg","BackKey","text","color","errorWords","acc","footer","StatCard","label","value"]}