posimai-root/posimai-sc/js/app.js

815 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DRILLS } from './data/drills.js';
import { CATEGORIES } from './data/categories.js';
function shuffleInPlace(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function extractTermsFromBeginnerBox(box) {
const clone = box.cloneNode(true);
clone.querySelector('.formula-label')?.remove();
const inner = clone.innerHTML.trim();
const parts = inner.split(/(?=<strong>)/i).map(s => s.trim()).filter(s => /^<strong>/i.test(s));
const terms = [];
for (const p of parts) {
const doc = new DOMParser().parseFromString('<div class="seg">'+p+'</div>', 'text/html');
const root = doc.querySelector('.seg');
const st = root && root.querySelector('strong');
if (!st) continue;
const term = st.textContent.trim();
let hint = '';
let n = st.nextSibling;
while (n) {
if (n.nodeType === 3) hint += n.textContent;
else if (n.nodeType === 1) hint += n.textContent;
n = n.nextSibling;
}
hint = hint.replace(/^[:\s]+/, '').trim();
if (term) terms.push({ term, hint });
}
return terms;
}
function parseConceptForGlossary(conceptHtml) {
if (!conceptHtml) return [];
const doc = new DOMParser().parseFromString('<div class="groot">'+conceptHtml+'</div>', 'text/html');
const root = doc.querySelector('.groot');
if (!root) return [];
const boxes = root.querySelectorAll('.formula-box');
for (const box of boxes) {
const lab = box.querySelector(':scope > .formula-label');
if (!lab || lab.textContent.indexOf('初学者向け') === -1) continue;
return extractTermsFromBeginnerBox(box);
}
return [];
}
function buildGlossaryRows(categories) {
const rows = [];
for (const cat of categories) {
for (const u of cat.units) {
const terms = parseConceptForGlossary(u.concept);
for (const t of terms) {
rows.push({
term: t.term,
hint: t.hint,
unitId: u.id,
num: u.num,
title: u.title,
catLabel: cat.label
});
}
}
}
rows.sort((a, b) => a.term.localeCompare(b.term, 'ja'));
return rows;
}
function sanitizeTrustedHtml(dirty) {
if (dirty == null || dirty === '') return '';
const s = String(dirty);
if (typeof window.DOMPurify === 'undefined') {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
return window.DOMPurify.sanitize(s, {
USE_PROFILES: { html: true, svg: true, svgFilters: true },
ALLOW_UNKNOWN_PROTOCOLS: false
});
}
const sanitizeStoredHtml = sanitizeTrustedHtml;
document.addEventListener('alpine:init', () => {
Alpine.data('scApp', () => ({
safeHtml(s) {
return sanitizeTrustedHtml(s);
},
categories:[],
currentUnit:null,
quizState:{},
search:'',
sidebarOpen:false,
isDark:true,
progress:{},
scores:{},
wrongUnits:{},
keys:['A','B','C','D','E'],
// concept fold — open by default so diagrams are immediately visible
conceptExpanded:true,
// step mode
stepMode:false,
stepStep:1,
stepKpIdx:0,
stepFlashRevealed:false,
stepDrillIdx:0,
stepDrillAnswered:false,
stepDrillSelected:-1,
// weak drill
weakDrillActive:false,
weakDrillUnits:[],
weakDrillUnitIdx:0,
weakDrillQuizState:{},
weakDrillResults:[],
weakDrillDone:false,
glossaryRows:[],
glossaryViewActive:false,
glossarySearch:'',
examActive:false,
examPhase:'setup',
examQuestions:[],
examIdx:0,
examQuizState:{},
init(){
this.categories = CATEGORIES.map(cat=>({
...cat,
units: cat.units.map(u=>({...u, catLabel:cat.label}))
}));
this.glossaryRows = buildGlossaryRows(this.categories);
try{ this.progress = JSON.parse(localStorage.getItem('posimai-sc-progress')||'{}'); }
catch{ this.progress = {}; }
try{ this.scores = JSON.parse(localStorage.getItem('posimai-sc-scores')||'{}'); }
catch{ this.scores = {}; }
try{ this.wrongUnits = JSON.parse(localStorage.getItem('posimai-sc-wrong')||'{}'); }
catch{ this.wrongUnits = {}; }
const t = localStorage.getItem('posimai-sc-theme')||'system';
this.isDark = t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);
const uid=(new URLSearchParams(location.search).get('unit')||'').trim().toLowerCase();
if(uid){
const u=this.allUnits().find(x=>x.id===uid);
if(u){ this.currentUnit=u; this.quizState={}; }
else{ try{ const url=new URL(window.location.href); url.searchParams.delete('unit'); const q=url.searchParams.toString(); history.replaceState(null,'',url.pathname+(q?'?'+q:'')+url.hash); }catch(e){} }
}
this.syncUnitToUrl();
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
this.$watch('search', ()=>{
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
});
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/sw.js').catch(()=>{});
}
document.addEventListener('keydown',(e)=>{
const t=e.target;
if(t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable)) return;
if(e.key==='/'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){
e.preventDefault();
const gs=document.querySelector('.glossary-search-input');
if(this.glossaryViewActive&&gs){ gs.focus(); return; }
const si=document.querySelector('.search-input');
if(si) si.focus();
}
});
},
syncUnitToUrl(){
try{
const url=new URL(window.location.href);
if(this.currentUnit&&this.currentUnit.id) url.searchParams.set('unit',this.currentUnit.id);
else url.searchParams.delete('unit');
const q=url.searchParams.toString();
const path=url.pathname+(q?'?'+q:'')+url.hash;
if(path!==window.location.pathname+window.location.search+window.location.hash)
history.replaceState(null,'',path);
}catch(e){}
},
goHome(){
this.currentUnit=null;
this.quizState={};
this.conceptExpanded=false;
this.stepMode=false;
this.weakDrillActive=false;
this.glossaryViewActive=false;
this.glossarySearch='';
this.examActive=false;
this.examPhase='setup';
this.examQuestions=[];
this.examIdx=0;
this.examQuizState={};
this.syncUnitToUrl();
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
get filteredCats(){
if(!this.search) return this.categories;
const q=this.search.toLowerCase();
return this.categories.map(cat=>({
...cat,
units:cat.units.filter(u=>u.title.toLowerCase().includes(q)||u.num.toLowerCase().includes(q))
})).filter(cat=>cat.units.length>0);
},
get filteredGlossary(){
const q=(this.glossarySearch||'').trim().toLowerCase();
if(!q) return this.glossaryRows;
return this.glossaryRows.filter(r=>{
return r.term.toLowerCase().includes(q)||r.hint.toLowerCase().includes(q)
||r.num.toLowerCase().includes(q)||r.title.toLowerCase().includes(q)
||r.catLabel.toLowerCase().includes(q);
});
},
get totalCount(){ return this.categories.reduce((s,c)=>s+c.units.length,0); },
get doneCount(){ return Object.keys(this.progress).length; },
get progressPct(){ return this.totalCount ? Math.round(this.doneCount/this.totalCount*100) : 0; },
get quizAnswered(){
let n=0;
Object.values(this.quizState).forEach(s=>{ if(s) n++; });
return n;
},
get quizCorrect(){
let n=0;
Object.values(this.quizState).forEach(s=>{ if(s?.correct) n++; });
return n;
},
// Persistent totals shown on home screen (from saved scores)
get totalBestCorrect(){
return Object.values(this.scores).reduce((s,v)=>s+(v?.best||0),0);
},
get totalBestPossible(){
return Object.values(this.scores).reduce((s,v)=>s+(v?.total||0),0);
},
catPct(cat){
if(!cat.units.length) return 0;
return Math.round(cat.units.filter(u=>this.progress[u.id]).length/cat.units.length*100);
},
allUnits(){ return this.categories.flatMap(c=>c.units); },
isDone(id){ return !!this.progress[id]; },
markDone(){
if(!this.currentUnit) return;
this.progress[this.currentUnit.id]=true;
localStorage.setItem('posimai-sc-progress',JSON.stringify(this.progress));
},
bestScore(id){
const s=this.scores[id];
if(!s) return '';
return s.best+'/'+s.total;
},
scoreChipCls(id){
const s=this.scores[id];
if(!s) return 'zero';
if(s.best===s.total) return '';
if(s.best>=s.total*0.6) return 'partial';
return 'zero';
},
saveScore(id,correct,total){
const prev=this.scores[id];
if(!prev||correct>prev.best){
this.scores[id]={best:correct,total};
localStorage.setItem('posimai-sc-scores',JSON.stringify(this.scores));
}
},
get freqBadgeCls(){
const f=this.currentUnit?.freq;
if(f==='high') return 'badge badge-high';
if(f==='mid') return 'badge badge-mid';
return 'badge badge-base';
},
get freqLabel(){
const f=this.currentUnit?.freq;
if(f==='high') return '頻出';
if(f==='mid') return '重要';
return '基礎';
},
get diffLabel(){
const d=this.currentUnit?.diff||1;
return '難易度 '+'★'.repeat(d)+'☆'.repeat(3-d);
},
openUnit(unit){
this.glossaryViewActive=false;
this.examActive=false;
this.currentUnit=unit;
this.quizState={};
this.conceptExpanded=true;
this.stepMode=false;
this.syncUnitToUrl();
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({top:0,behavior:'smooth'});
});
},
// Weak unit tracking
hasWrong(id){ return !!this.wrongUnits[id]; },
_onAllAnswered(){
if(!this.currentUnit) return;
const uid=this.currentUnit.id;
const total=this.currentUnit.quiz?.length||0;
const correct=Object.values(this.quizState).filter(s=>s?.correct).length;
this.saveScore(uid,correct,total);
if(correct>=total){
delete this.wrongUnits[uid];
// auto-mark done on perfect score
if(!this.progress[uid]){
this.progress[uid]=true;
try{ localStorage.setItem('posimai-sc-progress',JSON.stringify(this.progress)); }catch(e){}
}
} else {
this.wrongUnits[uid]=(this.wrongUnits[uid]||0)+1;
}
try{ localStorage.setItem('posimai-sc-wrong',JSON.stringify(this.wrongUnits)); }catch(e){}
this.wrongUnits={...this.wrongUnits};
},
get weakUnits(){
return this.allUnits().filter(u=>this.wrongUnits[u.id]>0)
.sort((a,b)=>(this.wrongUnits[b.id]||0)-(this.wrongUnits[a.id]||0));
},
get todayUnits(){
const result=[];
const weak=this.weakUnits;
if(weak.length>0) result.push({...weak[0], todayTag:'苦手'});
const all=this.allUnits();
const weakId=result[0]?.id;
const review=all.find(u=>!this.wrongUnits[u.id] && this.scores[u.id] && u.id!==weakId && !this.progress[u.id]);
if(review) result.push({...review, todayTag:'復習'});
if(result.length<2){
const next=all.find(u=>!this.progress[u.id] && u.id!==weakId && !result.find(r=>r.id===u.id));
if(next) result.push({...next, todayTag:'未学習'});
}
return result;
},
// Quiz
answered(qi){ return !!this.quizState[qi]; },
doAnswer(qi,ci,correct,exp){
if(this.quizState[qi]) return;
this.quizState[qi]={selected:ci,correct:ci===correct,exp:sanitizeStoredHtml(exp)};
this.quizState={...this.quizState};
const total=this.currentUnit?.quiz?.length||0;
if(total>0 && Object.keys(this.quizState).length>=total){
this._onAllAnswered();
}
},
qCardCls(qi){
const s=this.quizState[qi];
if(!s) return '';
return s.correct?'correct':'wrong';
},
choiceCls(qi,ci,correct){
const s=this.quizState[qi];
if(!s) return '';
if(ci===s.selected && s.correct) return 'sel-ok';
if(ci===s.selected && !s.correct) return 'sel-ng';
if(ci===correct && !s.correct) return 'rev-ok';
return '';
},
get scoreLabel(){
const total=this.currentUnit?.quiz?.length||0;
const answered=Object.keys(this.quizState).length;
const correct=Object.values(this.quizState).filter(s=>s?.correct).length;
if(!answered) return total+'問';
return correct+'/'+answered+'問正解';
},
get allAnswered(){
const total=this.currentUnit?.quiz?.length||0;
return total>0 && Object.keys(this.quizState).length>=total;
},
get resultScore(){
const correct=Object.values(this.quizState).filter(s=>s?.correct).length;
const total=this.currentUnit?.quiz?.length||0;
return correct+' / '+total;
},
get resultMsg(){
const correct=Object.values(this.quizState).filter(s=>s?.correct).length;
const total=this.currentUnit?.quiz?.length||0;
if(correct===total) return '完璧です!次の単元へ進みましょう。';
if(correct>=total*0.8) return 'よくできました。あと少しで完璧です。';
if(correct>=total*0.6) return '合格ラインです。間違えた問題の解説を確認しましょう。';
return '解説をよく読んで、もう一度チャレンジしてみましょう。';
},
resetQuiz(){ this.quizState={}; },
// Navigation
currentIdx(){ return this.allUnits().findIndex(u=>u.id===this.currentUnit?.id); },
get hasPrev(){ return this.currentIdx()>0; },
get hasNext(){ return this.currentIdx()<this.allUnits().length-1; },
prevUnit(){
const i=this.currentIdx();
if(i>0) this.openUnit(this.allUnits()[i-1]);
},
nextUnit(){
const i=this.currentIdx();
const all=this.allUnits();
if(i<all.length-1) this.openUnit(all[i+1]);
},
toggleTheme(){
this.isDark=!this.isDark;
const val=this.isDark?'dark':'light';
document.documentElement.setAttribute('data-theme',val);
try{ localStorage.setItem('posimai-sc-theme',val); }catch(e){}
},
// ---- Concept expand ----
get conceptPreview(){
const c=this.currentUnit?.concept||'';
const i=c.indexOf('</p>');
return i===-1 ? c : c.substring(0,i+4);
},
get conceptRest(){
const c=this.currentUnit?.concept||'';
const i=c.indexOf('</p>');
return i===-1 ? '' : c.substring(i+4);
},
// ---- Weak drill ----
get weakDrillCandidates(){
return this.allUnits()
.filter(u=>this.hasWrong(u.id))
.sort((a,b)=>{
const sa=this.scores[a.id]?.best||0;
const sb=this.scores[b.id]?.best||0;
return sa-sb;
})
.slice(0,5);
},
openGlossary(){
this.examActive=false;
this.glossaryViewActive=true;
this.glossarySearch='';
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const g=document.querySelector('.glossary-search-input');
if(g) g.focus();
});
},
closeGlossary(){
this.glossaryViewActive=false;
this.glossarySearch='';
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
openUnitFromGlossary(uid){
const u=this.allUnits().find(x=>x.id===uid);
if(u){
this.glossaryViewActive=false;
this.openUnit(u);
}
},
openExam(){
this.glossaryViewActive=false;
this.examActive=true;
this.examPhase='setup';
this.examQuestions=[];
this.examIdx=0;
this.examQuizState={};
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
exitExam(){
this.examActive=false;
this.examPhase='setup';
this.examQuestions=[];
this.examIdx=0;
this.examQuizState={};
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
fillExamQuestions(){
const pool=[];
const pushFreq=f=>{
for(const u of this.allUnits()){
if(u.freq!==f) continue;
(u.quiz||[]).forEach((q,qi)=>{ pool.push({ unitId:u.id, qi, unit:u, q }); });
}
};
pushFreq('high');
if(pool.length<10) pushFreq('mid');
if(pool.length<10){
for(const u of this.allUnits()){
if(u.freq==='high'||u.freq==='mid') continue;
(u.quiz||[]).forEach((q,qi)=>{ pool.push({ unitId:u.id, qi, unit:u, q }); });
}
}
shuffleInPlace(pool);
this.examQuestions=pool.slice(0, Math.min(10, pool.length));
},
startExamRun(){
this.fillExamQuestions();
if(!this.examQuestions.length) return;
this.examPhase='run';
this.examIdx=0;
this.examQuizState={};
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({ top:0, behavior:'auto' });
});
},
get examCurrentEntry(){ return this.examQuestions[this.examIdx]||null; },
get examTotal(){ return this.examQuestions.length; },
exAnswered(){
return !!this.examQuizState[this.examIdx];
},
exQCardCls(){
const s=this.examQuizState[this.examIdx];
if(!s) return '';
return s.correct?'correct':'wrong';
},
exChoiceCls(ci,correct){
const s=this.examQuizState[this.examIdx];
if(!s) return '';
if(ci===s.selected&&s.correct) return 'sel-ok';
if(ci===s.selected&&!s.correct) return 'sel-ng';
if(ci===correct&&!s.correct) return 'rev-ok';
return '';
},
doExamAnswer(ci,correct,exp){
if(this.examQuizState[this.examIdx]) return;
this.examQuizState[this.examIdx]={
selected:ci,
correct:ci===correct,
exp:sanitizeStoredHtml(exp)
};
this.examQuizState={...this.examQuizState};
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
nextExamQ(){
if(this.examIdx<this.examQuestions.length-1){
this.examIdx++;
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({ top:0, behavior:'smooth' });
});
} else {
this.examPhase='result';
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({ top:0, behavior:'smooth' });
});
}
},
get examCorrectCount(){
let n=0;
for(let i=0;i<this.examQuestions.length;i++){
if(this.examQuizState[i]?.correct) n++;
}
return n;
},
get examWrongItems(){
const out=[];
for(let i=0;i<this.examQuestions.length;i++){
const s=this.examQuizState[i];
if(s&&!s.correct){
const e=this.examQuestions[i];
out.push({ idx:i, unitId:e.unitId, num:e.unit.num, title:e.unit.title, q:e.q.q });
}
}
return out;
},
openUnitFromExam(uid){
const u=this.allUnits().find(x=>x.id===uid);
if(u){
this.exitExam();
this.openUnit(u);
}
},
startWeakDrill(){
this.glossaryViewActive=false;
this.examActive=false;
this.weakDrillUnits=[...this.weakDrillCandidates];
this.weakDrillUnitIdx=0;
this.weakDrillQuizState={};
this.weakDrillResults=[];
this.weakDrillDone=false;
this.weakDrillActive=true;
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
exitWeakDrill(){
this.weakDrillActive=false;
this.weakDrillDone=false;
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
get weakDrillCurrentUnit(){
return this.weakDrillUnits[this.weakDrillUnitIdx]||null;
},
wdAnswered(qi){ return !!this.weakDrillQuizState[qi]; },
wdQCardCls(qi){
const s=this.weakDrillQuizState[qi];
if(!s) return '';
return s.correct?'correct':'wrong';
},
wdChoiceCls(qi,ci,correct){
const s=this.weakDrillQuizState[qi];
if(!s) return '';
if(ci===s.selected&&s.correct) return 'sel-ok';
if(ci===s.selected&&!s.correct) return 'sel-ng';
if(ci===correct&&!s.correct) return 'rev-ok';
return '';
},
doWeakDrillAnswer(qi,ci,correct,exp){
if(this.weakDrillQuizState[qi]) return;
this.weakDrillQuizState[qi]={selected:ci,correct:ci===correct,exp:sanitizeStoredHtml(exp)};
this.weakDrillQuizState={...this.weakDrillQuizState};
},
get weakDrillAllAnswered(){
const total=this.weakDrillCurrentUnit?.quiz?.length||0;
return total>0 && Object.keys(this.weakDrillQuizState).length>=total;
},
get weakDrillUnitScore(){
const correct=Object.values(this.weakDrillQuizState).filter(s=>s?.correct).length;
const total=this.weakDrillCurrentUnit?.quiz?.length||0;
return correct+' / '+total;
},
nextWeakDrillUnit(){
const unit=this.weakDrillCurrentUnit;
if(!unit) return;
const correct=Object.values(this.weakDrillQuizState).filter(s=>s?.correct).length;
const total=unit.quiz?.length||0;
this.saveScore(unit.id,correct,total);
this.weakDrillResults.push({unitId:unit.id,num:unit.num,unitTitle:unit.title,correct,total});
if(correct>=total){
delete this.wrongUnits[unit.id];
localStorage.setItem('posimai-sc-wrong',JSON.stringify(this.wrongUnits));
this.wrongUnits={...this.wrongUnits};
}
if(this.weakDrillUnitIdx<this.weakDrillUnits.length-1){
this.weakDrillUnitIdx++;
this.weakDrillQuizState={};
} else {
this.weakDrillDone=true;
}
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
get weakDrillClearCount(){
return this.weakDrillResults.filter(r=>r.correct>=r.total).length;
},
// ---- Step mode ----
startStepMode(){
this.stepMode=true;
this.stepStep=1;
this.stepKpIdx=0;
this.resetFlashRevealState();
this.stepDrillIdx=0;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({top:0,behavior:'smooth'});
});
},
startStepModeClearQuiz(){
this.quizState={};
this.startStepMode();
},
exitStepMode(){
this.stepMode=false;
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
finishStepMode(){
this.stepMode=false;
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const el=document.getElementById('comprehension-quiz');
if(el) el.scrollIntoView({behavior:'smooth',block:'start'});
});
},
scrollToComprehension(){
this.$nextTick(()=>{
const el=document.getElementById('comprehension-quiz');
if(el) el.scrollIntoView({behavior:'smooth',block:'start'});
if(window.lucide) lucide.createIcons();
});
},
stepGoBack(){
if(!this.stepMode) return;
if(this.stepStep===1){
if(this.stepKpIdx>0){
this.stepKpIdx--;
this.resetFlashRevealState();
} else {
this.exitStepMode();
}
} else if(this.stepStep===2){
if(this.stepDrillIdx>0){
this.stepDrillIdx--;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
} else {
const kps=this.currentUnit?.keypoints||[];
this.stepStep=1;
this.stepKpIdx=Math.max(0,kps.length-1);
this.resetFlashRevealState();
}
} else if(this.stepStep===3){
const drills=this.unitDrills;
if(drills.length>0){
this.stepStep=2;
this.stepDrillIdx=drills.length-1;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
} else {
const kps=this.currentUnit?.keypoints||[];
this.stepStep=1;
this.stepKpIdx=Math.max(0,kps.length-1);
this.resetFlashRevealState();
}
}
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({top:0,behavior:'smooth'});
});
},
stepPipCls(n){
if(this.stepStep===n) return 's-active';
if(this.stepStep>n) return 's-done';
return '';
},
_kpDelimiterIndex(kp){
if(!kp||typeof kp!=='string') return -1;
const ja=kp.indexOf('');
if(ja>=0) return ja;
let depth=0;
for(let i=0;i<kp.length;i++){
const ch=kp[i];
if(ch==='<') depth++;
else if(ch==='>') depth=Math.max(0,depth-1);
else if(ch===':'&&depth===0) return i;
}
return -1;
},
keypointHasFlip(idx){
const kp=(this.currentUnit?.keypoints||[])[idx];
const i=this._kpDelimiterIndex(kp);
return i>0;
},
keypointTerm(idx){
const kp=(this.currentUnit?.keypoints||[])[idx];
const i=this._kpDelimiterIndex(kp);
return i>0 ? kp.slice(0,i).trim() : '';
},
resetFlashRevealState(){
this.stepFlashRevealed=!this.keypointHasFlip(this.stepKpIdx);
},
advanceStep1Card(){
if(this.stepStep!==1) return;
if(this.keypointHasFlip(this.stepKpIdx)&&!this.stepFlashRevealed){
this.stepFlashRevealed=true;
return;
}
this.nextStepFlash();
},
nextStepFlash(){
const kps=this.currentUnit?.keypoints||[];
if(this.stepKpIdx<kps.length-1){
this.stepKpIdx++;
this.resetFlashRevealState();
} else {
const drills=DRILLS[this.currentUnit?.id]||[];
this.stepStep=drills.length>0 ? 2 : 3;
this.stepDrillIdx=0;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
if(this.stepStep===3){
this.$nextTick(()=>{
const m=document.getElementById('main');
if(m) m.scrollTo({top:m.scrollHeight,behavior:'smooth'});
});
}
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
}
},
get unitDrills(){
return DRILLS[this.currentUnit?.id]||[];
},
doStepDrill(ci){
if(this.stepDrillAnswered) return;
this.stepDrillSelected=ci;
this.stepDrillAnswered=true;
},
nextStepDrill(){
const drills=this.unitDrills;
if(this.stepDrillIdx<drills.length-1){
this.stepDrillIdx++;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
} else {
this.stepStep=3;
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({top:m.scrollHeight,behavior:'smooth'});
});
}
}
}));
});