import { DRILLS } from './data/drills.js'; import { CATEGORIES } from './data/categories.js'; const API_BASE = 'https://api.soar-enrich.com/brain/api'; const APP_ID = 'posimai-sc'; const SYNC_KEY_LS = 'posimai_api_key'; 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(/(?=)/i).map(s => s.trim()).filter(s => /^/i.test(s)); const terms = []; for (const p of parts) { const doc = new DOMParser().parseFromString('
'+p+'
', '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('
'+conceptHtml+'
', '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:{}, syncToken:'', syncEnabled:false, syncBusy:false, syncStatus:'未同期', syncLastError:'', syncStateAt:0, syncRemoteAt:0, syncTimer:null, 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 = {}; } this.syncStateAt = parseInt(localStorage.getItem('posimai-sc-state-at') || '0', 10) || 0; const jwtToken = (localStorage.getItem('posimai_token') || '').trim(); const apiKey = (localStorage.getItem(SYNC_KEY_LS) || '').trim(); this.syncToken = jwtToken || apiKey; this.syncEnabled = !!this.syncToken; 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(this.syncEnabled){ this.pullCloudState().catch(()=>{}); } else { this.syncStatus = '同期キー未設定'; } 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){} }, nowTs(){ return Date.now(); }, get localStateBlob(){ return { progress: this.progress || {}, scores: this.scores || {}, wrongUnits: this.wrongUnits || {}, currentUnitId: this.currentUnit?.id || '', stateAt: this.syncStateAt || this.nowTs() }; }, applyStateBlob(blob){ const nextProgress = (blob && typeof blob.progress === 'object') ? blob.progress : {}; const nextScores = (blob && typeof blob.scores === 'object') ? blob.scores : {}; const nextWrong = (blob && typeof blob.wrongUnits === 'object') ? blob.wrongUnits : {}; this.progress = { ...nextProgress }; this.scores = { ...nextScores }; this.wrongUnits = { ...nextWrong }; try{ localStorage.setItem('posimai-sc-progress', JSON.stringify(this.progress)); }catch(e){} try{ localStorage.setItem('posimai-sc-scores', JSON.stringify(this.scores)); }catch(e){} try{ localStorage.setItem('posimai-sc-wrong', JSON.stringify(this.wrongUnits)); }catch(e){} if(blob && blob.currentUnitId){ const u = this.allUnits().find(x => x.id === blob.currentUnitId); if(u) this.currentUnit = u; } this.syncStateAt = parseInt(String(blob?.stateAt || this.nowTs()), 10) || this.nowTs(); try{ localStorage.setItem('posimai-sc-state-at', String(this.syncStateAt)); }catch(e){} this.syncUnitToUrl(); }, markStateChanged(){ this.syncStateAt = this.nowTs(); try{ localStorage.setItem('posimai-sc-state-at', String(this.syncStateAt)); }catch(e){} if(this.syncEnabled) this.scheduleCloudPush(); }, get syncHeader(){ const token = (localStorage.getItem('posimai_token') || '').trim() || (this.syncToken || '').trim(); return token ? { Authorization: 'Bearer ' + token } : null; }, async pullCloudState(){ const hdr = this.syncHeader; if(!hdr){ this.syncEnabled = false; this.syncStatus = '同期キー未設定'; return; } this.syncBusy = true; this.syncStatus = '同期を確認中...'; this.syncLastError = ''; try{ const res = await fetch(`${API_BASE}/app-state/${APP_ID}`, { headers: hdr }); if(res.status===404){ this.syncEnabled = true; this.syncStatus = 'クラウド初期化前(この端末の状態を保存できます)'; return; } if(!res.ok) throw new Error('HTTP '+res.status); const data = await res.json(); const remoteState = data?.state || {}; const remoteAt = parseInt(String(remoteState?.stateAt || 0), 10) || 0; this.syncRemoteAt = remoteAt; if(remoteAt > (this.syncStateAt || 0)){ this.applyStateBlob(remoteState); this.syncStatus = 'クラウドから最新状態を復元'; } else if(remoteAt > 0){ this.syncStatus = 'この端末の状態が最新'; } else { this.syncStatus = 'クラウド初期化前(この端末の状態を保存できます)'; } }catch(e){ this.syncLastError = String(e?.message || e || '同期失敗'); this.syncStatus = '同期エラー'; }finally{ this.syncBusy = false; } }, async pushCloudState(){ const hdr = this.syncHeader; if(!hdr || !this.syncEnabled) return; this.syncBusy = true; this.syncStatus = 'クラウドへ保存中...'; this.syncLastError = ''; try{ const payload = this.localStateBlob; const res = await fetch(`${API_BASE}/app-state/${APP_ID}`, { method:'PUT', headers:{ 'Content-Type':'application/json', ...hdr }, body: JSON.stringify({ state: payload }) }); if(!res.ok) throw new Error('HTTP '+res.status); this.syncRemoteAt = payload.stateAt; this.syncStatus = 'クラウド同期済み'; }catch(e){ this.syncLastError = String(e?.message || e || '同期失敗'); this.syncStatus = '同期エラー'; }finally{ this.syncBusy = false; } }, scheduleCloudPush(){ if(this.syncTimer) clearTimeout(this.syncTimer); this.syncTimer = setTimeout(()=>{ this.pushCloudState().catch(()=>{}); }, 800); }, enableSyncWithInput(){ const token = (this.syncToken || '').trim(); if(!token){ this.syncStatus = '同期キー未設定'; return; } localStorage.setItem(SYNC_KEY_LS, token); this.syncEnabled = true; this.pullCloudState().catch(()=>{}); }, async syncNow(){ if(!this.syncEnabled){ this.syncStatus = '同期キー未設定'; return; } await this.pushCloudState(); await this.pullCloudState(); }, 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.markStateChanged(); 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)); this.markStateChanged(); }, 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)); this.markStateChanged(); } }, 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.markStateChanged(); 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){} this.markStateChanged(); } } 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}; this.markStateChanged(); }, 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()0) this.openUnit(this.allUnits()[i-1]); }, nextUnit(){ const i=this.currentIdx(); const all=this.allUnits(); if(i'); return i===-1 ? c : c.substring(0,i+4); }, get conceptRest(){ const c=this.currentUnit?.concept||''; const i=c.indexOf('

'); 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{ 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;ix.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}; this.markStateChanged(); } if(this.weakDrillUnitIdx{ 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(); }); }, scrollToComprehension(){ if(document.activeElement && typeof document.activeElement.blur === 'function'){ document.activeElement.blur(); } this.$nextTick(()=>{ requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ this.scrollMainToComprehension('auto'); setTimeout(()=>{ this.scrollMainToComprehension('auto'); }, 140); }); }); if(window.lucide) lucide.createIcons(); }); }, scrollMainToComprehension(behavior='auto'){ const m=document.getElementById('main'); const el=document.getElementById('comprehension-quiz'); if(!m||!el) return; try{ el.focus({ preventScroll:true }); }catch(e){} const firstQ=el.querySelector('.q-card'); const target=firstQ||el; // main内の実座標で算出して、カード先頭(Q1)へ安定して合わせる const offset=8; const top=(target.getBoundingClientRect().top - m.getBoundingClientRect().top) + m.scrollTop - offset; m.scrollTo({ top:Math.max(0,top), behavior }); }, 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') 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.stepKpIdx0 ? 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{ if(window.lucide) lucide.createIcons(); const m=document.getElementById('main'); if(m) m.scrollTo({top:m.scrollHeight,behavior:'smooth'}); }); } } })); });