🌌 星辰大海

🚀 0光年
0
🌍 地球探索者
不求此生匆匆过,但求每天都成长!
每一个知识点,都是宇宙中的一颗星星
点亮它们,编织属于你的星图
🔭 练题 约2分钟

每次5道轻量练习,保持手感~

📡 AI 智能问答
拍张照问问,或者随便聊聊~ 学习上的事我随时在 😊(纯娱乐八卦,豆包更在行哦)
💡 长按对话可删除单条 · 点「清空对话」清除全部
🛸
学习上的问题,尽管问我~
📊
📁
'; var blob=new Blob(['\ufeff'+html],{type:'application/msword'}); var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=title+'.doc';a.click(); toast('已导出 '+title); } async function exportStageDoc(subject){ try{ toast('📄 正在生成 Word...'); var r=await fetch(API+'/study/export?subject='+encodeURIComponent(subject)); if(!r.ok){toast('导出失败~');return;} var d=await r.json(); var errors=d.errors||[];var mats=d.materials||[]; if(errors.length===0&&mats.length===0){toast('这个阶段还没有可导出的内容~');return;} var title=(d.stage_name||('第'+d.stage+'阶段'))+'学习档案'+(subject&&subject!=='all'?('·'+subjName(subject)):'·全科'); var html=''+title+''; html+='

'+title+'

'; html+='

导出时间:'+new Date().toLocaleString('zh-CN')+'

'; if(mats.length>0){ html+='

一、本阶段资料('+mats.length+' 份)

'; html+=''; mats.forEach(function(m){html+='';}); html+='
名称科目类型日期
'+m.title+''+(m.subject||'')+''+(m.type==='exam_paper'?'考卷':'作业')+''+(m.created_at||'').slice(0,10)+'
'; } if(errors.length>0){ var gs={};errors.forEach(function(e){(gs[e.subject||'其他']=gs[e.subject||'其他']||[]).push(e);}); html+='

'+(mats.length>0?'二':'一')+'、错题清单(共 '+errors.length+' 道,已攻克 '+errors.filter(function(e){return e.mastered;}).length+' 道)

'; Object.keys(gs).forEach(function(s){ html+='

'+subjName(s)+'('+gs[s].length+' 道)

'; gs[s].forEach(function(e,i){ html+='
'; html+='
'+(i+1)+'. '+(e.question_text||'')+'
'; html+='
知识点:'+(e.topic||'')+'|我的答案:'+(e.student_answer||'未作答')+'|正确答案:'+(e.correct_answer||'')+'
'; if(e.hint)html+='
💡 '+e.hint+'
'; html+='
'+(e.mastered?'✅ 已攻克':'🌑 待攻克')+'
'; html+='
'; }); }); } html+=''; var blob=new Blob(['\ufeff'+html],{type:'application/msword'}); var a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=title+'.doc';a.click(); toast('✅ 已导出 '+title+'.doc'); }catch(e){toast('导出失败~');} } function goBack(){ if(D.fromPracticePrompt){ D.fromPracticePrompt=false; showView('setup'); return; } showView(D.prevView||'home'); } // AI管家:加载学习概览 async function loadStudyOverview(){ var box=document.getElementById('managerContent'); if(!box)return; box.innerHTML='
🤖
小助手正在整理你的学习数据...
'; try{ var r=await fetch(API+'/study/overview'); if(!r.ok){box.innerHTML='
数据加载失败,请稍后再试~
';return;} var data=await r.json(); renderStudyOverview(data); }catch(e){ box.innerHTML='
网络波动,请稍后再试~
'; } } function renderStudyOverview(data){ var box=document.getElementById('managerContent'); if(!box)return; var s=data.stats||{}; var topics=data.error_topics||[]; var mastery=data.mastery_list||[]; var kmap=data.knowledge_map||[]; var suggestions=data.suggestions||[]; var lessons=data.micro_lessons||[]; var uploads=data.recent_uploads||[]; var h=''; // 今日状态卡片 h+='
' h+='
🧚
小助手 · 你只管练,整理交给我
每天自动帮你:归纳当天知识点、把上传的错题归类进错题本、归档资料。下面是它整理出的最新情况。
' h+='
' h+='
'; h+='
📊 今日学习状态
'; h+='
'; h+='
'+(s.unmastered_errors||0)+'
未掌握错题
'; h+='
'+(s.mastered_errors||0)+'
已掌握
'; h+='
'+(s.total_mastery||0)+'
已练知识点
'; h+='
'+(s.streak||0)+'
连续天数
'; h+='
'; // AI建议区 if(suggestions.length>0){ h+='
'; h+='
🧚 小助手建议
'; suggestions.forEach(function(sg,i){ h+='
'; h+='
'+(i+1)+'. '+(sg.action||'')+'
'; h+='
'+(sg.detail||'')+'
'; h+='
'; }); h+='
'; } // 微讲解 if(lessons.length>0){ h+='
'; h+='
💡 知识点微讲
'; lessons.forEach(function(l){ h+='
'; h+='
📖 '+(l.topic||'')+'
'; h+='
'+(l.lesson||'')+'
'; if(l.example)h+='
例:'+l.example+'
'; h+='
'; }); h+='
'; } // 知识地图 if(kmap.length>0){ h+='
'; h+='
🗺️ 知识地图
'; kmap.forEach(function(k){ var colors={weak:'#E05050',reviewing:'#F5A623',mastered:'#3DA85C'}; var labels={weak:'⚠️ 薄弱',reviewing:'🔁 复习中',mastered:'✅ 已掌握'}; var c=colors[k.status]||'#8A99AD'; var l=labels[k.status]||''; h+='
'; h+='
'; h+='
'+(k.topic||'')+'
'; h+='
'+l; if(k.error_count>0)h+=' · 错'+k.error_count+'题'; if(k.recall_count>0)h+=' · 复习'+k.recall_count+'次'; h+='
'; }); h+='
'; } // 快捷入口 h+='
'; h+='
⚡ 快捷入口
'; h+='
'; h+=''; h+=''; h+=''; h+='
'; box.innerHTML=h; } // ═══ AI 训练师卡:女儿调教 AI 的成果反馈(本地渲染,零 AI 调用)═══ function buildTrainerLearnNote(n,bias,todayN,logs){ var parts=[]; if(n<10){ parts.push('正从你每次点「太简单/刚好/够呛」里,学怎么读懂你的真实节奏'); }else if(bias>0.2){ parts.push('因为你总点「够呛」,它把题往下收了'); }else if(bias<-0.2){ parts.push('因为你总点「太简单」,它往上加料了'); }else{ parts.push('因为你点得有难有易,它把难度调匀了'); } if(todayN>=3) parts.push('今天你教了'+todayN+'次,这一轮节奏它记牢了'); if(logs&&logs.length){ var lt=(logs[0]&&logs[0].text)?String(logs[0].text):''; if(lt) parts.push('尤其是你刚说「'+lt+'」,它已经记住'); } return parts.join(';'); } function buildTrainerWhy(n,bias,todayN,logs){ var p=[]; p.push('你每次做完题点「太简单 / 刚好 / 够呛」,都等于告诉 AI 这道题对你难不难。'); if(n<10){ p.push('你现在点得还不多,它还在建立对你的判断,多点几次就更准。'); }else if(bias>0.2){ p.push('你最近点「够呛」偏多,AI 算出来你嫌题难,所以它把题往简单调。'); }else if(bias<-0.2){ p.push('你最近点「太简单」偏多,AI 算出来你嫌题水,所以它往上加料。'); }else{ p.push('你点得有难有易,AI 把难度调到了正贴你节奏的位置。'); } if(todayN>=1) p.push('今天你已经教了它 '+todayN+' 次,这一轮它记得最清楚。'); p.push('你喂得越多、越真实,它越懂你——你才是那个训练它的人。'); return p.join(''); } function toggleTrainerWhy(span){ var w=span.nextElementSibling; if(w.style.display==='block'){w.style.display='none';span.textContent='?为什么';} else{w.style.display='block';span.textContent='收起';} } async function renderTrainerCard(scope){ scope=scope||'home'; var el=document.getElementById('homeSummaryText'); if(!el)return; if(!D.profile||!(D.profile.feedback_profile||D.profile.stats)){ try{var pr=await fetch(API+'/profile');if(pr.ok)D.profile=await pr.json();}catch(e){} } var fp=(D.profile&&D.profile.feedback_profile)||{}; var logsAll=[];try{logsAll=JSON.parse(localStorage.getItem('wd_log')||'[]')||[];}catch(e){} var isDaily=false; var logs=logsAll; var n,bias; if(isDaily){ n=logs.length; bias=0; } else { n=+(fp.training_count||0); bias=parseFloat(fp.difficulty_bias||0)||0; } var lv=1+Math.floor(n/10), prog=n%10; var day0=new Date();day0.setHours(0,0,0,0); var todayN=logs.filter(function(x){return x&&x.t>=day0.getTime();}).length; var head; if(n===0){ head = isDaily ? '日常学习这边还没教过它 🐣
做每日任务、改错题时顺手点一下,它就记住你的节奏' : '它还不太懂你 🐣
做完题顺手点一下「太简单 / 刚好 / 够呛」,它就开始贴你的节奏'; } else if(!isDaily && bias<-0.2){ head='它把题出得偏简单了 🌿
碰到「够呛」的题就点出来,它会悄悄加点料'; } else if(!isDaily && bias>0.2){ head='它出题有点上强度了 🔥
觉得「太简单」就直说,它会给你放点水'; } else if(isDaily){ head='日常学习里你教了它 '+n+' 次 🌱
每次卡点、错因、预习回执,它都记下了'; } else { head='难度正贴着你的节奏走 🌟
继续把真实感受喂给它,它会越来越准'; } var stars='';for(var i=0;i<10;i++){stars+=i'+stars+''+(n===0?'从第一颗星开始':(prog===0?'刚升级 🎉':'再教 '+(10-prog)+' 次升级'))+''; h+='
'+head+'
'; var learnNote = buildTrainerLearnNote(n,bias,todayN,logs); var whyNote = buildTrainerWhy(n,bias,todayN,logs); h+='
最近它学会了 · '+wEsc(learnNote)+' ?为什么
'; h+='
'+wEsc(whyNote)+'
'; if(logs.length){ logs.slice(0,1).forEach(function(x){ var d=new Date(x.t);var ds=(d.getMonth()+1)+'/'+d.getDate(); h+='
🌱 '+wEsc(x.text||'')+' · '+ds+'
'; }); }else{ h+='
'+(isDaily?'日常学习里你还没点过 · 做任务时顺手点一下,它就记在这':'还没有——你告诉它的话,都会记在这')+'
'; } el.innerHTML=h; if(!isDaily){var ov=document.getElementById('homeOverview');if(ov)ov.style.display='block';} } async function loadHome(){ try{const r=await fetch(API+'/profile');if(!r.ok)return;D.profile=await r.json();const s=D.profile.stats||{};D.streak=s.current_streak||0;D.coins=D.coins||(s.total_diagnoses||0)*5; // Apply baseline (user can reset stats) const bl=JSON.parse(localStorage.getItem('statsBaseline')||'{"diagnoses":0,"questions":0,"correct":0,"streak":0,"coins":0}'); const dCount=Math.max(0,(s.total_diagnoses||0)-bl.diagnoses); const tQ=Math.max(0,(s.total_questions||0)-bl.questions); const tC=Math.max(0,(s.total_correct||0)-bl.correct); const streak=Math.max(0,(s.current_streak||0)-bl.streak); D.coins=Math.max(0,D.coins-bl.coins); const acc=tQ>0?Math.round(tC/tQ*100):'--'; document.getElementById('streakTop').textContent=streak; document.getElementById('coinTop').textContent=D.coins; // AI 训练师卡(本地渲染:默契等级+难度评价+最近学会,不再调 AI 生成) renderTrainerCard('home'); // 更新分析报告&学习档案预览 try{ var wps2=D.profile?(D.profile.weak_points||[]):[]; var s2=D.profile?(D.profile.stats||{}):{}; var ap2=document.getElementById('homeAnalysisPreview'); var ar2=document.getElementById('homeArchivePreview'); if(ap2){var rl2=loadReports();ap2.textContent=rl2.length>0?('最近:'+((rl2[0].dateLabel||'').slice(5,11))+' '+(rl2[0].subjectName||'综合')):'练完就能看小结';} if(ar2)ar2.textContent=loadReports().length>0?'练题错题与训练轨迹':'完成练习自动归档'; }catch(e){} // Star map (always shows current weak points from profile) const wps=D.profile.weak_points||[]; if(document.getElementById('starmap')&&wps.length>0){document.getElementById('starmap').innerHTML=wps.slice(0,8).map((w,i)=>{const colors=['var(--sol)','var(--sirius)','var(--nebula)','var(--nova-teal)'];return `
${(w.topic||w).slice(0,8)}
`;}).join('');}}catch(e){} } // P0体感优化:加载提示轮播(出题/批改等待时按阶段切换文案,降低等待焦虑) let _hintTimer=null; function hintRotate(msgs){stopHint();const el=document.getElementById('progHint');if(!el||!msgs||!msgs.length)return;el.textContent=msgs[0];let i=0;_hintTimer=setInterval(()=>{i=(i+1)%msgs.length;el.textContent=msgs[i];},4000);} function stopHint(){if(_hintTimer){clearInterval(_hintTimer);_hintTimer=null;}} async function startExplore(){ showView('explore');document.getElementById('btnNext').classList.remove('hidden');document.getElementById('btnFinish').classList.add('hidden');document.getElementById('progFill').style.width='0%';hintRotate(['🧭 正在锁定你的薄弱知识点...','✨ AI 正在为你定制新题...','🧮 正在验算数学答案...','🚀 星图马上绘制完成...']); try{const ctrl=new AbortController();const tout=setTimeout(()=>ctrl.abort(),180000);const r=await fetch(API+'/diagnosis/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({subject:D.settings.subject,question_count:D.settings.count,difficulty:D.settings.difficulty,mood:D.settings.mood}),signal:ctrl.signal});clearTimeout(tout);const d=await r.json();if(!r.ok){stopHint();document.getElementById('progHint').textContent='😅 宇宙辐射干扰,信号中断...';setTimeout(()=>showView('home'),2000);return}D.questions=d.questions||[];D.answers={};D.curQ=0;if(D.questions.length===0){stopHint();toast('今天的星图还没绘制好~ 稍后再来吧~');showView('home');return}stopHint();document.getElementById('progHint').textContent='准备好了吗?开始答题!';updateProg();renderQ();renderQFeedback(D.questions[D.curQ]);loadTrainer();}catch(e){stopHint();const msg=e.name==='AbortError'?'光速有限,信号还在路上~':'宇宙波动,刷新一下就好啦~';document.getElementById('progHint').textContent='😅 '+msg;setTimeout(()=>showView('home'),2500);} } function renderQ(){ if(D.curQ>=D.questions.length)return;const q=D.questions[D.curQ];const sc={'math':'tag-math','chinese':'tag-chinese','english':'tag-english'};const dl={1:'⭐ 近星',2:'⭐⭐ 远星',3:'⭐⭐⭐ 暗星',4:'🔥 黑洞'};const tl={choice:'选择',fill:'填空',short_answer:'简答',calculation:'计算'};const qt=q.type||'choice'; const ss=q.topic_source;const ssl=ss==='weak'?'🎯 你的薄弱点':ss==='cold_start'?'📝 摸底题':ss==='school_high_freq'?'📚 课本高频':'';const ssTag=ssl?`${ssl}`:''; let ih='';if(qt==='choice'&&q.options&&q.options.length>0){ih=`
${q.options.map((o,i)=>{const sel=D.answers[q.id]===o.label?' selected':'';return `
${o.label}${o.text}
`;}).join('')}
`;}else if(qt==='fill'){ih=`
`;}else if(qt==='short_answer'){ih=``;}else if(qt==='calculation'){ih=``;}document.getElementById('qContainer').innerHTML=`
${q.subject_name||''}${dl[q.difficulty]||''}${tl[qt]||qt}${ssTag}
📖 ${q.topic||''}
${q.question}
${ih}
`; } function pick(qid,opt){D.answers[qid]=opt;renderQ();}function typeA(qid,val){D.answers[qid]=val;} function updateProg(){const t=D.questions.length,c=D.curQ+1;document.getElementById('progFill').style.width=(c/t*100)+'%';document.getElementById('progNum').textContent=`第 ${c}/${t} 题`;} function nextQ(){if(D.curQ0){D.curQ--;updateProg();renderQ();renderQFeedback(D.questions[D.curQ]);document.getElementById('btnNext').classList.remove('hidden');document.getElementById('btnFinish').classList.add('hidden');}} async function finish(){ const answers=D.questions.map(q=>({question_id:q.id,student_answer:D.answers[q.id]||'',question_text:q.question,correct_answer:q.correct_answer,topic:q.topic,subject:q.subject,difficulty:q.difficulty,type:q.type||''})); hintRotate(['🔭 AI 正在批改...','🧮 主观题正在复核...','🚀 分数马上出炉...']); try{const r=await fetch(API+'/diagnosis/grade',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({answers})});stopHint();if(!r.ok){const e=await r.json();toast('数据解析异常: '+(e.detail||'请重试'));return}const d=await r.json();D.coins=(D.coins||0)+5;document.getElementById('coinTop').textContent=D.coins;const sc=d.score||0,mx=d.max_score||D.questions.length*10;document.getElementById('resScore').textContent=sc;document.getElementById('resSub').textContent=`满分 ${mx}`;const pct=mx>0?Math.round(sc/mx*100):0;let emoji,msg;if(pct>=90){emoji='🌟';msg='你点燃了一颗超新星!宇宙级的闪耀!';celebrate();}else if(pct>=70){emoji='🌠';msg='星光璀璨!更多的星星等待你去点亮!';}else if(pct>=50){emoji='💫';msg='稳健的航程!看看哪些星球还需要能量~';}else{emoji='🌌';msg='每一颗巨星都从星际尘埃开始!继续探索吧~';}document.getElementById('resMsg').innerHTML=emoji+' '+msg; const details=d.details||[];let dh='
📋 答题详情
'; answers.forEach((a,i)=>{const dt=details[i]||{};const ok=dt.is_correct!==false;const icon=ok?'⭐':'🌑';const bg=ok?'rgba(102,224,122,0.06)':'rgba(240,98,110,0.05)';const bd=ok?'var(--nova-green)':'rgba(240,98,110,0.5)';const sq=(D.questions[i]&&D.questions[i].topic_source);const sql=sq==='weak'?' 🎯薄弱点':sq==='cold_start'?' 📝摸底题':sq==='school_high_freq'?' 📚课本高频':'';dh+=`
${icon} 第${i+1}题 · ${a.topic||''}${sql}
${(a.question_text||'').substring(0,120)}
你的答案:${a.student_answer||'未作答'}✅ 正确答案:${a.correct_answer||'?'}
${!ok?`
💡 ${dt.feedback||'这道题还需要巩固~'}
`:''}
`;}); const wps=d.weak_points||[];document.getElementById('resWeak').innerHTML=dh+'
🔍 AI 分析报告
'+(wps.length>0?wps.map(w=>`
📌 ${w.topic||''}(${w.error_type||'需要关注'})
薄弱程度:${w.severity||'中'}
💡 ${w.suggestion||'多练习几次,慢慢就掌握了~'}
`).join(''):'
🎉 全部答对!你是学习小达人~
');saveReport({subject:D.settings.subject||'all',score:sc,max:mx,pct:pct,details:answers.map((a,i)=>({topic:a.topic,question_text:a.question_text,student_answer:a.student_answer,correct_answer:a.correct_answer,subject:a.subject,ok:(details[i]||{}).is_correct!==false,feedback:(details[i]||{}).feedback||''})),weak:d.weak_points||[]}); // 把"做对的知识点"写入间隔复习池(招三:治练了就忘——做对也按节奏再考) try{const okItems=D.questions.filter((q,i)=>(details[i]||{}).is_correct!==false).map(q=>({topic:q.topic,subject:q.subject||'math'})).filter(x=>x.topic&&x.topic.length<40);if(okItems.length){fetch('/api/mastery/mark',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items:okItems})}).catch(()=>{});}}catch(e){} showView('result');}catch(e){stopHint();toast('星图分析出错,刷新再来~');} } // ── 练题反馈:训练 AI 更懂你(成就感/粘性)── function renderQFeedback(q){ var box=document.getElementById('qFeedback'); if(!box)return; if(!q||!q.id){box.innerHTML='';return;} var tid=q.id, top=encodeURIComponent(q.topic||''), sub=q.subject||'math'; var fs='style="border:1px solid var(--border);background:#fff;border-radius:16px;padding:5px 10px;font-size:12px;cursor:pointer;color:var(--text)"'; box.innerHTML='
' +'
👉 顺手点一下,训练 AI 出更懂你的题
' +'
' +'' +'' +'' +'' +'
' +'
'; } function toggleFbMore(tid,top,sub){var b=document.getElementById('fbmore_'+tid); if(b){ if(b.style.display==='none'){b.style.display='flex';}else{b.style.display='none';} }} function sendFb(tid,top,sub,felt,scope,quality){ scope=scope||''; quality=quality||''; if(!felt&&!scope&&!quality)return; fetch(API+'/student/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question_id:tid,topic:decodeURIComponent(top),subject:sub,difficulty_felt:felt,scope:scope,quality:quality})}).then(function(r){return r.json();}).then(function(d){ var box=document.getElementById('qFeedback'); if(box)box.innerHTML='
✨ AI 记下啦!它正在变得更懂你~
'; if(d&&d.level)updateTrainerBadge(d.training_count,d.level); if(felt){var _fm={too_hard:'太难',just_right:'适中',too_easy:'太简单'};var _ft=_fm[felt];if(_ft)wdLog('练题反馈','练题 · 你说「'+_ft+'」','home');} }).catch(function(){}); } function updateTrainerBadge(n,lv){var b=document.getElementById('trainerBadge'); if(b)b.textContent='🎓 AI训练师 Lv.'+lv+'(已训练'+n+'次)';} function loadTrainer(){fetch(API+'/profile').then(function(r){return r.json();}).then(function(p){var fp=(p&&p.feedback_profile)||{};var n=fp.training_count||0;updateTrainerBadge(n,1+Math.floor(n/10));}).catch(function(){});} // ── 报告档案(localStorage 持久化)── function saveReport(r){ const subs={math:'数学',chinese:'语文',english:'英语',all:'综合'}; const now=new Date(),p=n=>String(n).padStart(2,'0'); const label=`${now.getFullYear()}-${p(now.getMonth()+1)}-${p(now.getDate())} ${p(now.getHours())}:${p(now.getMinutes())}`; const rec=Object.assign({id:'r'+now.getTime()+Math.random().toString(36).slice(2,6),date:now.toISOString(),dateLabel:label,subjectName:subs[r.subject]||'综合'},r); const arr=loadReports();arr.unshift(rec); localStorage.setItem('practice_reports',JSON.stringify(arr.slice(0,60))); } function loadReports(){try{var arr=JSON.parse(localStorage.getItem('practice_reports')||'[]');var dirty=false;arr.forEach(function(r){if(!r||!Array.isArray(r.details))return;if(!r.id){r.id='r'+Date.now().toString(36)+Math.random().toString(36).slice(2,6);dirty=true;}var fb=(r.subject&&r.subject!=='all')?r.subject:'all';(r.details||[]).forEach(function(d){if(d&&(!d.subject||!/^(math|chinese|english|all)$/.test(d.subject))){d.subject=fb;dirty=true;}});});if(dirty){try{localStorage.setItem('practice_reports',JSON.stringify(arr.slice(0,60)));}catch(e){}}return arr;}catch(e){return[];}} function renderReports(){ const box=document.getElementById('reportList');if(!box)return; const arr=loadReports();const cnt=document.getElementById('reportCount');if(cnt)cnt.textContent='共 '+arr.length+' 份'; if(arr.length===0){box.innerHTML='
📂
还没有报告档案~ 完成一次练习就会自动归档到这里
';return;} const subs={math:'数学',chinese:'语文',english:'英语',all:'综合'}; const groups={}; arr.forEach(function(r){ const bySubj={}; (r.details||[]).forEach(function(d){const s=d.subject||r.subject||'all';(bySubj[s]=bySubj[s]||[]).push(d);}); Object.keys(bySubj).forEach(function(s){(groups[s]=groups[s]||[]).push({r:r,n:bySubj[s].length});}); }); const order=['math','chinese','english','all']; let h=''; order.filter(function(s){return groups[s];}).forEach(function(sub){ h+='
📂 '+subs[sub]+' ('+groups[sub].length+'次)
'; groups[sub].forEach(function(g){ const r=g.r;const total=(r.details||[]).length;const tag=g.n
'+(r.source==='exam'?'📝 试卷':'')+r.score+'/'+r.max+' · '+r.pct+'%
' +'
📅 '+r.dateLabel+tag+'
' +'' +''; }); }); box.innerHTML=h; } function delReport(id){ if(!confirm('确定删除这份报告吗?测试或过时的报告可随时清理。'))return; const arr=loadReports().filter(x=>x.id!==id); localStorage.setItem('practice_reports',JSON.stringify(arr)); renderReports();toast('🗑 已删除'); } function exportReport(id){ const r=loadReports().find(x=>x.id===id);if(!r)return; const subs={math:'数学',chinese:'语文',english:'英语',all:'综合'};const subj=subs[r.subject]||r.subject; let qRows=''; (r.details||[]).forEach((a,i)=>{ qRows+='' +''+(i+1)+(a.ok?' ✅':' ❌')+'' +'' +'
知识点:'+wEsc(a.topic||'—')+'
' +'
'+wEsc(a.question_text||'')+'
' +'
我的答案:'+wEsc(a.student_answer||'未作答')+'
' +(a.ok?'':'
正确答案:'+wEsc(a.correct_answer||'')+'
') +((a.ok||!a.feedback)?'':'
💡 解析:'+wEsc(a.feedback)+'
') +''; }); let weakRows=''; (r.weak||[]).forEach(w=>{ weakRows+=''+wEsc(w.topic||'')+'' +''+wEsc(w.error_type||'')+'' +''+wEsc(w.suggestion||'')+''; }); const stars=(r.details||[]).map(a=>a.ok?'★':'☆').join(''); const docHtml='练习报告' +'' +'

🌌 星辰大海 · 练习报告

' +'
科目:'+wEsc(subj)+' | 日期:'+wEsc(r.dateLabel||'')+' | 得分:'+r.score+' / '+r.max+'('+r.pct+'%) | 表现:'+stars+'
' +'

一、答题详情

'+qRows+'
' +(weakRows?'

二、AI 薄弱分析与建议

'+weakRows+'
知识点问题类型建议
':'') +'
本报告由 星辰大海 AI 学习助手 自动生成
' +''; const blob=new Blob(['\ufeff'+docHtml],{type:'application/msword;charset=utf-8'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='练习报告_'+subj+'_'+(r.dateLabel||'').replace(/[: ]/g,'-')+'.doc';a.click();URL.revokeObjectURL(a.href); toast('📄 Word 报告已导出'); } async function loadErrors(){ // v6: ALWAYS start from API, never trust localStorage for data var items=[]; var savedUI={}; try{savedUI=JSON.parse(localStorage.getItem('upload_errors_ui')||'{}')||{};}catch(e){} try{ var r=await fetch('/api/errors?limit=500'); if(r.ok){ var d=await r.json(); var errs=d.errors||[]; errs.forEach(function(e){ if(e.source_type!=='upload')return; var uiState=savedUI[e.id]||{mode:'view',revealed:false,mastered:false,val:'',collapsed:true}; items.push({backendId:e.id,sourceType:'upload',sourceTitle:e.source_title||'',sourcePage:e.source_page||0,questionNum:e.question_num||'',key:'api-'+e.id,reportId:null,detailIndex:null,subject:subjCode(e.subject),dateLabel:(e.added_at||'').slice(0,10),topic:e.topic||'待分析',question_text:e.question_text||'',student_answer:e.student_answer||'未作答',correct_answer:e.correct_answer||'',feedback:(e.error_type||'')+(e.hint?(';'+e.hint):''),solution:e.solution||'',recallCount:e.recall_count||0,ui:uiState}); }); } }catch(e){} D.wrongItems=items; renderErrors(); } // 把前端错题项(尤其「在线练习」来源的)映射到后端 errors 表的 error_id, // 这样删除/已掌握才能落到实处(否则后端那份拷贝会在回顾练习里「复活」)。 async function reconcileBackendIds(){ try{ const r=await fetch('/api/errors?limit=500');const d=await r.json(); const list=d.errors||[]; const byText={}; list.forEach(function(e){const k=wNorm(e.question_text||'');if(k){(byText[k]=byText[k]||[]).push(e);}}); let changed=false; (D.wrongItems||[]).forEach(function(it){ if(it.backendId)return; const k=wNorm(it.question_text||'');if(!k)return; const cand=(byText[k]||[]).find(function(e){return wNorm(e.student_answer||'')===wNorm(it.student_answer||'');})||(byText[k]||[])[0]; if(cand){it.backendId=cand.id;if(cand.recall_count)it.recallCount=cand.recall_count;changed=true;} }); if(changed)renderErrors(); }catch(e){} } function wNorm(s){return (s||'').toString().trim().toLowerCase().replace(/[\s,。、?!.,?!;;::]/g,'');} function wEsc(s){return (s||'').replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});} function renderErrors(){ const box=document.getElementById('errGroups');if(!box)return; const cnt=document.getElementById('errCount');const items=D.wrongItems||[]; if(cnt)cnt.textContent=items.length>0?('共 '+items.length+' 道'):'上传的错题都汇集在这,清完就销账'; if(items.length===0){box.innerHTML='
🌌
还没有错题~ 传作业或试卷后,AI 识别的错题会自动汇集到这里
';return;} let h=''; var todoIdx=[]; items.forEach(function(it,idx2){if(!it.ui.mastered&&todoIdx.length<3)todoIdx.push(idx2);}); if(todoIdx.length>0){ var unTotal=0;items.forEach(function(it){if(!it.ui.mastered)unTotal++;}); h+='
'; h+='
🎯 今天该清的 共'+unTotal+'道 · 变式连对2次自动✅
'; todoIdx.forEach(function(idx2){ var it=items[idx2]; var prog=it.recallCount>0?(' ⭐'+Math.min(2,it.recallCount)+'/2'):''; var btn=it.backendId?(''):(''); h+='
'; h+='
🌑 '+wEsc(it.topic||'错题')+prog+'
'+btn+'
'; }); h+='
'; } const subs={math:'数学',chinese:'语文',english:'英语',all:'综合'}; if(!D.errGroupCollapsed)D.errGroupCollapsed={}; const groups={};items.forEach(function(it,idx){(groups[it.subject]=groups[it.subject]||[]).push(idx);}); Object.keys(groups).forEach(function(sub){ const gc=!!D.errGroupCollapsed[sub]; h+='
' +'
📂 '+(subs[sub]||sub)+' ('+groups[sub].length+'道)
' +'
'+(gc?'▸':'▾')+'
'; if(!gc){groups[sub].forEach(function(idx){h+=wrongCard(idx);});} }); box.innerHTML=h; // v6: Save UI state only (not data) to localStorage try{var uiSave={};(D.wrongItems||[]).forEach(function(it){if(it.backendId&&it.ui)uiSave[it.backendId]=it.ui;});localStorage.setItem('upload_errors_ui',JSON.stringify(uiSave));}catch(e){} } function wrongCard(idx){ const it=D.wrongItems[idx];const u=it.ui; const subs={math:'数学',chinese:'语文',english:'英语',all:'综合'}; const mark=u.mastered?'':''; const srcTag=it.sourceType==='upload'?('
📤 来源:'+(wEsc(it.sourceTitle)||'上传')+' · '+(subs[it.subject]||it.subject)+(it.sourcePage?((' · 第'+it.sourcePage+'页')):'')+(it.questionNum?((' · '+it.questionNum)):'')+'
'):''; if(u.mode==='variant'){return variantCard(idx);} if(u.collapsed){ return '
'+'
'+mark+'🌑 '+(wEsc(it.topic)||'错题')+'
'+'
'; } const head='
'+'
'+mark+'🌑 '+(wEsc(it.topic)||'错题')+'
'+'
'; const analyzeBtn=''; const variantBtn=it.backendId?'':''; if(u.mode==='answer'){ return '
'+head+srcTag+'
'+wEsc(it.question_text)+'
'+''+'
'+''+analyzeBtn+variantBtn+''+''+'
'+(u.mastered?'
✅ 答对了!已掌握这道错题~
':(u.lastWrong?'
❌ 答错了,再想想~
':''))+wrongAnalysisBlock(idx)+'
'; } return '
'+head+srcTag+'
'+wEsc(it.question_text)+'
'+(u.mastered?'
✅ 已掌握
':'')+'
'+''+analyzeBtn+variantBtn+''+'
'+wrongAnalysisBlock(idx)+'
'; } function wrongAnalysisBlock(idx){ const it=D.wrongItems[idx];if(!it.ui.revealed)return ''; const solBtn=it.backendId?(''):''; const solBlock=it.solution?('
'+wEsc(it.solution)+'
'):''; return '
' +'
💡 分析与答案
' +'
正确答案:'+wEsc(it.correct_answer)+'
' +(it.feedback?'
'+wEsc(it.feedback)+'
':'') +solBlock+solBtn +'
你当时作答:'+wEsc(it.student_answer)+'
'; } async function genSolution(i){ const it=D.wrongItems[i]; if(!it||!it.backendId)return; const btn=window.event&&window.event.target; if(btn)btn.textContent='📝 AI 正在写解题过程...'; try{ const r=await fetch('/api/errors/'+encodeURIComponent(it.backendId)+'/explain'+(it.solution?'?force=1':''),{method:'POST'}); const d=await r.json(); if(d.ok&&d.solution){ it.solution=d.solution; renderErrors(); toast('✅ 解题过程已生成'); }else{ if(btn)btn.textContent='📝 查看解题过程'; toast('❌ '+(d.error||'生成失败,请稍后重试')); } }catch(e){ if(btn)btn.textContent='📝 查看解题过程'; toast('❌ 网络异常,请重试'); } } function redoWrong(i){D.wrongItems[i].ui={mode:'answer',revealed:false,mastered:false,val:'',collapsed:false};renderErrors();} function toggleWrong(i){D.wrongItems[i].ui.collapsed=!D.wrongItems[i].ui.collapsed;renderErrors();} function toggleErrGroup(sub){if(!D.errGroupCollapsed)D.errGroupCollapsed={};D.errGroupCollapsed[sub]=!D.errGroupCollapsed[sub];renderErrors();} async function deleteWrong(i){const it=D.wrongItems[i];if(!it)return;if(!confirm('确定从错题本删除这道题吗?测试误判或已彻底掌握的题可以删~'))return;if(it.backendId){try{await fetch('/api/errors/'+encodeURIComponent(it.backendId),{method:'DELETE'});}catch(e){}const arr=loadUploadErrors().filter(x=>x.backendId!==it.backendId);saveUploadErrors(arr);}D.wrongItems.splice(i,1);renderErrors();toast('已从错题本删除');} async function masterWrong(i){const it=D.wrongItems[i];if(!it)return; var doMaster=async function(){ if(it.backendId){try{await fetch('/api/errors/'+encodeURIComponent(it.backendId)+'/master',{method:'POST'});}catch(e){}const arr=loadUploadErrors().filter(x=>x.backendId!==it.backendId);saveUploadErrors(arr);}else{const reports=loadReports();const r=reports.find(x=>x.id===it.reportId);if(r&&r.details&&it.detailIndex!=null){r.details[it.detailIndex].wrongRemoved=true;localStorage.setItem('practice_reports',JSON.stringify(reports));}} D.wrongItems.splice(i,1);renderErrors();addDailyPts(2,'销账一道错题');toast('✅ 已标记为掌握,棒!'); }; wdAsk({hook:'wdmaster',title:'这题当时为什么错?',sub:'帮它变准 · 点一下就好',opts:[{v:'概念没懂',t:'概念没懂'},{v:'算错了',t:'算错了'},{v:'看错题',t:'看错题'}],skip:'跳过 · 直接销账',cb:function(c1){ if(c1===null){doMaster();return;} wdAsk({hook:'wdmaster2',noQuota:true,title:'老师讲过这个知识点了吗?',sub:'还没讲的错,不算你落后',opts:[{v:'讲过了',t:'讲过了'},{v:'还没讲',t:'还没讲'}],skip:'跳过 · 直接销账',cb:function(c2){ if(c2==='还没讲'){wdLog('超前暴露','「'+(it.topic||'这道题')+'」老师还没讲 · 不算掌握漏洞','daily');toast('好,先挂着——老师没讲过的错,不算你落后');} else if(c1){wdLog('错因自证','「'+(it.topic||'错题')+'」'+c1+(c2==='讲过了'?' · 老师讲过':''),'daily');} doMaster(); }}); }}); } function backWrong(i){D.wrongItems[i].ui.mode='view';renderErrors();} function showWrongAnalysis(i){D.wrongItems[i].ui.revealed=true;D.wrongItems[i].ui.lastWrong=false;renderErrors();} function submitRedo(i){ const it=D.wrongItems[i];const u=it.ui; if(wNorm(u.val)===wNorm(it.correct_answer)){u.mastered=true;u.mode='view';u.lastWrong=false;u.revealed=false;renderErrors();toast('✅ 答对了,已掌握!点「分析答案」可回顾~');} else{u.lastWrong=true;u.revealed=false;renderErrors();} } function variantCard(idx){ const it=D.wrongItems[idx];const u=it.ui;const q=u.variant_q||{}; const hasOpt=q.type==='choice'&&q.options&&q.options.length; let body='
'+wEsc(q.question||'(AI 正在出题…)')+'
'; if(hasOpt){ body+='
'; (q.options||[]).forEach(function(o){body+='';}); body+='
'; }else{ body+=''; } let ansLine=''; if(u.variant_reveal){ let ok=false; if(hasOpt){ok=(u.variant_ans||'').toUpperCase()===((''+q.correct_answer).toUpperCase());} else{ok=wNorm(u.variant_ans||'')===wNorm(''+q.correct_answer);} ansLine='
'+(ok?('✅ 答对了!'+((it.recallCount||0)>=2?'🎉 变式连对2次,已攻克~':'再连对1次就攻克啦~')):('❌ 还差点,正确答案:'+wEsc(''+q.correct_answer)+'(连对进度已重置,再来~'))+'
'; } return '
' +'
✨ 同类变式('+wEsc(q.topic||it.topic||'')+')
' +body +'
' +'' +'' +'
' +ansLine+'
'; } function variantWrong(idx){ const it=D.wrongItems[idx]; if(!it||!it.backendId){toast('这道题暂不支持变式~');return;} it.ui.mode='variant';it.ui.variant_q={};it.ui.variant_ans='';it.ui.variant_reveal=false;renderErrors(); fetch('/api/errors/'+encodeURIComponent(it.backendId)+'/variant',{method:'POST'}) .then(r=>r.json()) .then(d=>{if(d.error){toast('变式生成失败:'+d.error);it.ui.mode='view';}else{it.ui.variant_q=d.question||{};it.ui.variant_q.topic=d.topic||it.topic||'';}renderErrors();}) .catch(e=>{toast('变式生成失败,请重试');it.ui.mode='view';renderErrors();}); } function submitVariant(idx){ const it=D.wrongItems[idx];const u=it.ui;u.variant_reveal=true; var q=u.variant_q||{}; var hasOpt=q.type==='choice'&&q.options&&q.options.length; var ok=hasOpt?((u.variant_ans||'').toUpperCase()===(''+q.correct_answer).toUpperCase()):(wNorm(u.variant_ans||'')===wNorm(''+q.correct_answer)); if(ok){ it.recallCount=(it.recallCount||0)+1; if(it.recallCount>=2&&!u.mastered){u.mastered=true;applyVariantMastered(it);toast('🎉 变式连对2次,已攻克!');} else{toast('✅ 答对啦,再连对1次就攻克!');} }else{it.recallCount=0;} if(it.backendId){try{fetch('/api/errors/'+encodeURIComponent(it.backendId)+'/recall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({correct:ok})}).catch(function(){});}catch(e){}} renderErrors(); } function applyVariantMastered(it){ if(it.backendId){const arr=loadUploadErrors().filter(x=>x.backendId!==it.backendId);saveUploadErrors(arr);} if(it.reportId){const reports=loadReports();const r=reports.find(x=>x.id===it.reportId);if(r&&r.details&&it.detailIndex!=null){r.details[it.detailIndex].wrongRemoved=true;localStorage.setItem('practice_reports',JSON.stringify(reports));}} } function showVariantAnswer(idx){const it=D.wrongItems[idx];it.ui.variant_reveal=true;renderErrors();} function loadUploadErrors(){try{return (JSON.parse(localStorage.getItem('upload_errors')||'[]')||[]).filter(function(x){return x.sourceType==='upload';}).map(function(x){if(!x.ui)x.ui={mode:'view',revealed:false,mastered:false,val:'',collapsed:true};return x;});}catch(e){return[];}} function saveUploadErrors(arr){try{localStorage.setItem('upload_errors',JSON.stringify(arr.slice(0,500)));}catch(e){}} function syncUploadErrors(added){ if(!added||!added.length)return; const subs={math:'数学',chinese:'语文',english:'英语'}; const arr=loadUploadErrors();const have=new Set(arr.map(x=>x.backendId)); const news=added.map(function(e){ const subj=e.subject||'数学';const subjKey=Object.keys(subs).find(function(k){return subs[k]===subj;})||'math'; return {backendId:e.error_id,sourceType:'upload',sourceTitle:e.source_title||'',sourcePage:e.source_page||0,questionNum:e.question_num||'',key:'up-'+e.error_id,reportId:null,detailIndex:null,subject:subjKey,dateLabel:'上传',topic:e.topic||'待分析',question_text:e.question_text||'',student_answer:e.student_answer||'未作答',correct_answer:e.correct_answer||'',feedback:(e.error_type||'')+(e.hint?(';'+e.hint):''),solution:e.solution||'',ui:{mode:'view',revealed:false,mastered:false,val:'',collapsed:true}}; }); news.forEach(function(it){if(!have.has(it.backendId))arr.push(it);}); saveUploadErrors(arr); D.wrongItems=(D.wrongItems||[]).concat(news.filter(function(it){return !have.has(it.backendId);})); renderErrors(); toast('📒 已把 '+news.length+' 道错题收进错题本(含来源标注)'); } async function reviewWrong(){ try{ const r=await fetch('/api/errors/review?count=6&include_mastery=true',{method:'POST'});const d=await r.json(); const list=d.questions||[]; var uploadList=list.filter(function(q){return q.item_type==='mastery'||(q.source_type==='upload')||(q.source_title&&q.source_title.length>0&&q.source_type!=='practice'&&q.source_type!=='exam');}); if(!uploadList.length){toast('上传的错题都攻克了~ 去传一份新作业吧');return;} const box=document.getElementById('reviewList');let h=''; uploadList.forEach(function(q,idx){ if(q.item_type==='mastery'){ // 间隔复习题:做对但超 N 天未复习的知识点,巩固用 const mid='reviewMas_'+idx; h+='
' +'
🔄 间隔复习 · 已掌握但快忘了:'+(q.topic||'')+'
' +'
'+wEsc(q.question_text||'')+'
' +'' +'
正确答案:'+wEsc(q.correct_answer||'')+'
' +'
' +'
'; }else{ const eid=encodeURIComponent(q.error_id||''); const src=(q.source_title?('📤 '+q.source_title+(q.source_page?('·第'+q.source_page+'页'):'')):''); h+='
' +(src?'
'+wEsc(src)+'
':'') +'
'+wEsc(q.question_text||'')+'
' +'' +'
正确答案:'+wEsc(q.correct_answer||'')+''+(q.hint?('
'+wEsc(q.hint)+''):'')+'
' +'
' +'' +'' +'
' +'
'; } }); box.innerHTML=h; document.getElementById('reviewOverlay').classList.remove('hidden'); }catch(e){toast('回顾练习加载失败,请重试');} } function closeReview(){document.getElementById('reviewOverlay').classList.add('hidden');} async function masterRecall(mid,topic,subject){ // 间隔复习"记住了":更新复习时间,下次按更长节奏再考 try{await fetch('/api/mastery/recall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic:topic,subject:subject})});}catch(e){} const node=document.getElementById(mid);if(node)node.remove(); toast('🔄 已记入间隔复习,下次按节奏再考你~'); } async function deleteReviewError(eid){ eid=decodeURIComponent(eid); if(!confirm('确定删除这道题?误判/测试题可以删掉,删后不会在回顾与强化卷里再出现~'))return; try{await fetch('/api/errors/'+encodeURIComponent(eid),{method:'DELETE'});}catch(e){} const arr=loadUploadErrors().filter(x=>x.backendId!==eid);saveUploadErrors(arr); const node=document.getElementById('reviewItem_'+encodeURIComponent(eid));if(node)node.remove(); toast('🗑 已删除'); } async function masterReviewError(eid){ eid=decodeURIComponent(eid); if(!confirm('标记为已掌握?它将退出回顾练习与出题强化,但记录保留。'))return; try{await fetch('/api/errors/'+encodeURIComponent(eid)+'/master',{method:'POST'});}catch(e){} const arr=loadUploadErrors().filter(x=>x.backendId!==eid);saveUploadErrors(arr); const node=document.getElementById('reviewItem_'+encodeURIComponent(eid));if(node)node.remove(); toast('✅ 已标记为掌握'); } function loadSettings(){document.getElementById('sCount').value=D.settings.count;document.getElementById('vCount').textContent=D.settings.count;document.getElementById('sDiff').value={auto:0,easy:1,hard:2}[D.settings.difficulty]||0;document.getElementById('vDiff').textContent={auto:'自动',easy:'近地',hard:'深空'}[D.settings.difficulty]||'自动';document.getElementById('sSubj').value={all:0,math:1,chinese:2,english:3}[D.settings.subject]||0;document.getElementById('vSubj').textContent={all:'全星系',math:'数学',chinese:'语文',english:'英语'}[D.settings.subject]||'全星系';} document.getElementById('sCount').addEventListener('input',function(){D.settings.count=+this.value;document.getElementById('vCount').textContent=this.value;}); document.getElementById('sDiff').addEventListener('input',function(){D.settings.difficulty=['auto','easy','hard'][+this.value];document.getElementById('vDiff').textContent={auto:'自动',easy:'近地',hard:'深空'}[D.settings.difficulty];}); document.getElementById('sSubj').addEventListener('input',function(){D.settings.subject=['all','math','chinese','english'][+this.value];document.getElementById('vSubj').textContent={all:'全星系',math:'数学',chinese:'语文',english:'英语'}[D.settings.subject];}); document.querySelectorAll('#semesterPicker .mood-btn').forEach(b=>{if(b.id==='smPlacement')b.classList.add('selected');b.addEventListener('click',async function(){document.querySelectorAll('#semesterPicker .mood-btn').forEach(x=>x.classList.remove('selected'));this.classList.add('selected');D.semesterMode=this.dataset.mode;try{await fetch('/api/semester?mode='+D.semesterMode,{method:'POST'});}catch(e){}});}); document.getElementById('btnSave').addEventListener('click',()=>{if(D.fromPracticePrompt){D.fromPracticePrompt=false;startExplore();return;}showView(D.prevView||'home');toast('✨ 太空舱设定已保存!')}); document.getElementById('btnReset').addEventListener('click',()=>{ if(!confirm('确定要重置航行数据吗?后台数据不受影响。'))return; const s=D.profile?.stats||{}; localStorage.setItem('statsBaseline',JSON.stringify({ diagnoses:s.total_diagnoses||0,questions:s.total_questions||0, correct:s.total_correct||0,streak:s.current_streak||0,coins:D.coins||0 })); D.coins=0;showView('home');toast('🔄 航行数据已归零!后台数据完好'); }); document.getElementById('btnChangeAvatar').addEventListener('click',()=>document.getElementById('avatarFile').click()); document.getElementById('homeAvatar').addEventListener('click',()=>document.getElementById('avatarFile').click()); function changeAvatar(){const f=document.getElementById('avatarFile').files[0];if(!f)return;const r=new FileReader();r.onload=e=>{localStorage.setItem('userAvatar',e.target.result);document.getElementById('homeAvatar').src=e.target.result;document.getElementById('mascotTop').src=e.target.result;toast('🖼️ 头像已更换!');};r.readAsDataURL(f);} function loadAvatar(){const s=localStorage.getItem('userAvatar');if(s){document.getElementById('homeAvatar').src=s;document.getElementById('mascotTop').src=s;}} // Arena const ExamMode={selected:'placement'}; document.querySelectorAll('#durPick .star-btn').forEach(b=>b.addEventListener('click',function(){document.querySelectorAll('#durPick .star-btn').forEach(x=>x.classList.remove('selected'));this.classList.add('selected');document.getElementById('vExamDur').textContent=this.dataset.min+'分钟';})); document.getElementById('sExamSubj').addEventListener('input',function(){document.getElementById('vExamSubj').textContent=['数学','语文','英语'][+this.value];}); document.querySelectorAll('#examTypes button').forEach(b=>{b.addEventListener('click',function(){document.querySelectorAll('#examTypes button').forEach(x=>{x.style.background='';x.style.borderColor='var(--border)';});this.style.background='var(--sirius-lt)';this.style.borderColor='var(--sirius)';ExamMode.selected=this.dataset.exam;});}); document.getElementById('btnGenerateExam').addEventListener('click',generateExam); // ══════════════════════════════════════════════════════════════ // 试卷工场 v14:可屏幕作答 + 航行时间倒计时 + 交卷批改 + 错题归档 // ══════════════════════════════════════════════════════════════ let D_exam=null; // 当前试卷 JSON let D_examTimer=null; // 计时器句柄 let D_examDuration=60; // 航行时间(分钟) let D_examRemain=0; // 剩余秒数(支持离开页面暂停、回来恢复) let D_examSubmitted=false;// 防重复交卷 let D_examGrace=false; // 是否已进入「到点宽限」阶段 let EXAM_GRACE_SEC=300; // 到点后的宽限秒数(5分钟);后期适应了改成 0 即取消宽限、到点立即交卷 function examEsc(s){return (s==null?'':String(s)).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});} async function generateExam(){ document.getElementById('examMsg').textContent='🔭 正在生成试卷...'; const subj=['math','chinese','english'][+document.getElementById('sExamSubj').value]; const dur=+document.querySelector('#durPick .star-btn.selected').dataset.min; // 融合错题本薄弱点(AI 记住并针对性出同类题) let wt=''; try{const tr=await fetch('/api/errors/topics');const td=await tr.json();if(td.topics&&td.topics.length){wt=td.topics.slice(0,8).map(t=>t.topic).join(',');}}catch(e){} try{ const ctrl=new AbortController();const tout=setTimeout(()=>ctrl.abort(),180000); const url='/api/exam/generate?subject='+subj+'&duration='+dur+'&exam_type='+ExamMode.selected+(wt?('&weak_topics='+encodeURIComponent(wt)):''); const r=await fetch(url,{signal:ctrl.signal}); clearTimeout(tout); if(!r.ok){document.getElementById('examMsg').textContent='试卷生成失败,请重试';return;} const d=await r.json(); let msg='✅ '+d.exam_name+' · '+d.question_count+'题 · 建议'+d.time_suggestion; if(wt)msg+=' · 🎯已融入你的错题本薄弱点'; document.getElementById('examMsg').textContent=msg; renderExamPaper(d); startExamTimer(dur); }catch(e){document.getElementById('examMsg').textContent='生成超时,请重试';} } function renderExamPaper(d){ D_exam=d; const box=document.getElementById('examPaper'); box.classList.remove('hidden'); box.scrollIntoView({behavior:'smooth'}); let h='
'; h+='
'; h+='

📝 '+examEsc(d.exam_name)+'

'; h+='

'+examEsc(d.subject_name)+' · '+examEsc(d.description)+'

'; h+='

满分 '+d.total_score+'分 · 建议'+examEsc(d.time_suggestion)+' · '+d.question_count+'题

'; h+='
--:--
'; h+='
姓名:班级:
'; let qnum=0; (d.questions||[]).forEach((q,i)=>{ if(q.type==='section'){ h+='
'+examEsc(q.section_title)+'
'; return; } qnum++; const qid=q.id||('q'+i); h+='
'; h+='
'+qnum+'. ('+(q.points||0)+'分) '+examEsc(q.question||'')+'
'; if(q.type==='choice'&&q.options&&q.options.length){ h+='
'; q.options.forEach(function(o){h+='';}); h+='
'; }else if(q.type==='fill'){ h+='
'; }else{ h+='
'; } h+='
'; }); h+='
'; h+='
'; h+='
'; box.innerHTML=h; document.getElementById('btnSubmitExam').addEventListener('click',submitExam); document.getElementById('btnPrintExam').addEventListener('click',function(){window.print();}); } function collectExamAnswers(){ const d=D_exam;if(!d)return []; const out=[]; (d.questions||[]).forEach(function(q,i){ if(q.type==='section')return; let val=''; if(q.type==='choice'){const el=document.querySelector('input[name="ans_'+i+'"]:checked');val=el?el.value:'';} else{const el=document.querySelector('.exam-ans[data-i="'+i+'"]');val=el?el.value.trim():'';} out.push({id:q.id,student_answer:val,type:q.type,points:q.points||0,topic:q.topic||''}); }); return out; } async function submitExam(){ if(D_examSubmitted){return;} // 防重复交卷(手动交卷 vs 计时到点自动交卷) const ans=collectExamAnswers(); if(ans.length===0){toast('还没有题目可提交~');return;} D_examSubmitted=true; const btn=document.getElementById('btnSubmitExam'); if(btn){btn.disabled=true;btn.textContent='🔍 批改中...';} stopExamTimer(); try{ const subj=['math','chinese','english'][+document.getElementById('sExamSubj').value]; const r=await fetch('/api/exam/grade',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({subject:subj,answers:ans})}); const g=await r.json(); if(g.error){D_examSubmitted=false;alert('批改出错:'+g.error);if(btn){btn.disabled=false;btn.textContent='✅ 交卷批改';}return;} if(btn){btn.textContent='✅ 已交卷';} renderExamResult(g); saveExamReport(g); }catch(e){ D_examSubmitted=false; if(btn){btn.disabled=false;btn.textContent='✅ 交卷批改';} toast('批改失败,刷新再来~'); } } function renderExamResult(g){ const pct=g.max_score>0?Math.round(g.score/g.max_score*100):0; let h='
'; h+='
'+g.score+'
满分 '+g.max_score+' · 正确 '+g.correct_count+'/'+g.total_count+' · '+pct+'%
'; h+='
🤖 智能批改完成'+(g.ai_enabled?'(AI 语义批改已启用)':'(当前未配置 AI 密钥,解答题按答案比对,请对照参考答案自查)')+'
'; h+='
'; (g.details||[]).forEach(function(dt,idx){ const ok=dt.is_correct; const bg=ok?'rgba(102,224,122,0.06)':'rgba(240,98,110,0.05)'; const bd=ok?'var(--nova-green)':'rgba(240,98,110,0.5)'; h+='
'; h+='
'+(ok?'✅':'❌')+' 第'+(idx+1)+'题 · '+(dt.max_points||0)+'分
'; h+='
你的答案:'+examEsc(dt.student_answer||'未作答')+'
'; h+='
正确答案:'+examEsc(dt.correct_answer||'?')+'
'; if(dt.analysis)h+='
💡 解析:'+examEsc(dt.analysis)+'
'; if(!ok&&dt.feedback)h+='
'+examEsc(dt.feedback)+'
'; h+='
'; }); h+='
'; const res=document.getElementById('examResult'); if(res){res.innerHTML=h;res.scrollIntoView({behavior:'smooth'});} } function examTick(){ const el=document.getElementById('examTimer'); const m=Math.floor(D_examRemain/60),s=D_examRemain%60; if(el)el.textContent=(D_examGrace?'缓冲 ':'')+(m<10?'0':'')+m+':'+(s<10?'0':'')+s; if(el)el.style.color=(D_examGrace||D_examRemain<=Math.floor(D_examDuration*60*0.1))?'var(--nova-red)':''; if(D_examRemain<=0){ if(D_examGrace){stopExamTimer();if(el)el.textContent='交卷中';submitExam();return;} // 主倒计时到点:优先进入宽限(EXAM_GRACE_SEC>0),否则直接交卷 if(EXAM_GRACE_SEC>0){ D_examGrace=true;D_examRemain=EXAM_GRACE_SEC; if(el)el.textContent='缓冲 '+(Math.floor(EXAM_GRACE_SEC/60)<10?'0':'')+Math.floor(EXAM_GRACE_SEC/60)+':00'; toast('⏰ 时间到!再给你 '+Math.round(EXAM_GRACE_SEC/60)+' 分钟缓冲,抓紧作答~'); return; } stopExamTimer();if(el)el.textContent='时间到';submitExam();return; } D_examRemain--; } function startExamTimer(mins){ stopExamTimer(); D_examDuration=Math.max(1,mins||60); D_examRemain=D_examDuration*60; D_examSubmitted=false; D_examGrace=false; examTick(); D_examTimer=setInterval(examTick,1000); } function stopExamTimer(){if(D_examTimer){clearInterval(D_examTimer);D_examTimer=null;}} function pauseExamTimer(){stopExamTimer();} function resumeExamTimer(){if(!D_exam||D_examSubmitted||D_examRemain<=0||D_examTimer)return;D_examTimer=setInterval(examTick,1000);} function saveExamReport(g){ const subjSel=['math','chinese','english'][+document.getElementById('sExamSubj').value]; const subs={math:'数学',chinese:'语文',english:'英语',all:'综合'}; const now=new Date(),p=function(n){return String(n).padStart(2,'0');}; const label=now.getFullYear()+'-'+p(now.getMonth()+1)+'-'+p(now.getDate())+' '+p(now.getHours())+':'+p(now.getMinutes()); const qById={};(D_exam.questions||[]).forEach(function(q){if(q.id)qById[q.id]=q;}); const details=(g.details||[]).map(function(dt){ const q=qById[dt.id]||{}; return { topic:q.topic||'', question_text:q.question||'', student_answer:dt.student_answer||'未作答', correct_answer:dt.correct_answer||'?', subject:subjSel, ok:dt.is_correct, feedback:dt.feedback||'' }; }); const rec=Object.assign({id:'r'+now.getTime()+Math.random().toString(36).slice(2,6),date:now.toISOString(),dateLabel:label,subjectName:subs[subjSel]||'综合',subject:subjSel},{ score:g.score,max:g.max_score,pct:g.max_score>0?Math.round(g.score/g.max_score*100):0,details:details,weak:[],source:'exam' }); const arr=loadReports();arr.unshift(rec); localStorage.setItem('practice_reports',JSON.stringify(arr.slice(0,200))); toast('📒 试卷已归档,错题已收入错题本'); } // QA let qaImgB64=null; let qaImgUrl=null;let qaConvHistory=[]; function initQA(){ document.getElementById('btnQaSend').addEventListener('click',sendQA); document.getElementById('btnQaImg').addEventListener('click',()=>document.getElementById('qaImgFile').click()); document.getElementById('qaImgFile').addEventListener('change',function(){ const f=this.files[0]; if(!f)return; // 立即显示处理中提示 document.getElementById('qaImgPreview').innerHTML='
⏳ 正在处理图片...
'; const reader=new FileReader(); reader.onerror=()=>{ document.getElementById('qaImgPreview').innerHTML='
❌ 图片读取失败,请重拍
'; }; reader.onload=e=>{ const dataUrl=e.target.result; // 先直接设置 base64 和预览(确保立即可见,不依赖 Canvas 压缩) qaImgB64=dataUrl.split(',')[1]; qaImgUrl=dataUrl; document.getElementById('qaImgPreview').innerHTML='
✅ 已选择图片,点击发送'; // 异步尝试压缩(如果失败,使用原始 base64,不影响功能) try{ const img=new Image(); img.onload=()=>{ try{ const canvas=document.createElement('canvas'); const maxW=1280;let w=img.width,h=img.height; if(w>maxW){h=Math.round(h*maxW/w);w=maxW;} if(h>1280){w=Math.round(w*1280/h);h=1280;} canvas.width=w;canvas.height=h; canvas.getContext('2d').drawImage(img,0,0,w,h); const compressed=canvas.toDataURL('image/jpeg',0.85); qaImgB64=compressed.split(',')[1]; qaImgUrl=compressed; document.getElementById('qaImgPreview').innerHTML='
✅ 已选择图片,点击发送'; }catch(err){/* Canvas 压缩失败,保留原始 base64 */} }; img.onerror=()=>{/* Image 加载失败(如 HEIC 格式),保留原始 base64 */}; img.src=dataUrl; }catch(err){/* 整个压缩流程出错,保留原始 base64 */} }; reader.readAsDataURL(f); }); } async function sendQA(){ const inp=document.getElementById('qaInput'); const text=inp.value.trim(); if(!text&&!qaImgB64)return; const imgB64ToSend=qaImgB64; const imgUrlToSend=qaImgUrl; const userText=text||(imgB64ToSend?'(图片信号)':''); addQaBubble('user',userText,imgUrlToSend); inp.value=''; qaImgB64=null; qaImgUrl=null; document.getElementById('qaImgPreview').innerHTML=''; document.getElementById('qaImgFile').value=''; const form=new FormData(); form.append('question',text||(imgB64ToSend?'请识别图片中的题目并给出详细解答':'')); if(imgB64ToSend){form.append('image_base64',imgB64ToSend);} if(qaConvHistory.length>0){form.append('history',JSON.stringify(qaConvHistory.slice(-10)));} const loadingHtml=''; addQaBubble('ai',loadingHtml,null,true); const sendBtn=document.getElementById('btnQaSend'); sendBtn.disabled=true;sendBtn.textContent='...'; try{ const r=await fetch('/api/qa/ask',{method:'POST',body:form}); const d=await r.json(); document.getElementById('qaHistory').lastElementChild?.remove(); const reply=d.reply||'信号已收到~'; addQaBubble('ai',reply); qaConvHistory.push({role:'user',content:userText}); qaConvHistory.push({role:'assistant',content:reply}); if(qaConvHistory.length>20)qaConvHistory=qaConvHistory.slice(-20); }catch(e){ document.getElementById('qaHistory').lastElementChild?.remove(); addQaBubble('ai','深空通讯中断,稍后再试~'); } sendBtn.disabled=false;sendBtn.textContent='发送'; } function addQaBubble(role,text,imgUrl,isHtml){ const d=document.getElementById('qaHistory'); if(d.querySelector('div[style*="text-align:center"]'))d.innerHTML=''; const isU=role==='user'; const b=document.createElement('div'); b.className='qa-bubble'; b.style.cssText='margin-bottom:12px;display:flex;flex-direction:column;align-items:'+(isU?'flex-end':'flex-start')+';position:relative'; let html=''; if(imgUrl){html+='';} if(text){html+='
'+(isHtml?text:qaFormatText(text))+'
';} b.innerHTML=html; // 长按删除单条对话 let pressTimer=null; const delBubble=function(){ if(confirm('删除这条对话?')){b.remove();if(!d.querySelector('.qa-bubble')){d.innerHTML='
🛸
向宇宙发送信号吧~
';}} }; b.addEventListener('touchstart',function(e){pressTimer=setTimeout(delBubble,600);},{passive:true}); b.addEventListener('touchend',function(){clearTimeout(pressTimer);}); b.addEventListener('touchmove',function(){clearTimeout(pressTimer);}); b.addEventListener('contextmenu',function(e){e.preventDefault();delBubble();}); d.appendChild(b); b.scrollIntoView({behavior:'smooth'}); } function qaFormatText(text){ return text .replace(/&/g,'&') .replace(//g,'>') .replace(/\*\*(.+?)\*\*/g,'$1') .replace(/^---$/gm,'
') .replace(/^### (.+)$/gm,'$1') .replace(/^## (.+)$/gm,'$1') .replace(/\n/g,'
'); } let qaRecog=null; function initQaVoice(){ const SR=window.SpeechRecognition||window.webkitSpeechRecognition; const btn=document.getElementById('btnQaVoice'); if(!SR){ if(btn) btn.style.display='none'; return; } if(!qaRecog){ qaRecog=new SR(); qaRecog.lang='zh-CN'; qaRecog.interimResults=false; qaRecog.maxAlternatives=1; qaRecog.onresult=function(e){ const txt=e.results[0][0].transcript; const inp=document.getElementById('qaInput'); inp.value=(inp.value?inp.value+' ':'')+txt; inp.focus(); }; qaRecog.onend=function(){ if(btn) btn.textContent='🎤'; }; qaRecog.onerror=function(){ if(btn) btn.textContent='🎤'; }; } if(qaRecog && qaRecog.state==='recording'){ qaRecog.stop(); if(btn) btn.textContent='🎤'; } else { qaRecog.start(); if(btn) btn.textContent='🔴'; } } function clearQA(){ qaConvHistory=[]; qaImgB64=null; qaImgUrl=null; document.getElementById('qaImgPreview').innerHTML=''; document.getElementById('qaImgFile').value=''; document.getElementById('qaInput').value=''; document.getElementById('qaHistory').innerHTML='
🛸
学习上的问题,尽管问我~
'; } // ═══ 分析报告(轻松版:每题分析 + 小结讲解 + 错题重练)═══ var lightExpand={}; function lightSummary(r){ var ds=r.details||[]; var wrong=ds.filter(function(d){return !d.ok;}); var right=ds.filter(function(d){return d.ok;}); var rt=[];right.forEach(function(d){if(d.topic&&rt.indexOf(d.topic)<0)rt.push(d.topic);}); var lines=[]; if(wrong.length===0){ lines.push({c:'var(--nova-green)',t:'🌟 全部点亮!'+(rt.slice(0,2).join('、')||'知识点')+'掌握得很扎实,继续保持~'}); return lines; } if(rt.length)lines.push({c:'var(--nova-green)',t:'👍 '+rt.slice(0,3).join('、')+' 掌握得不错'}); var wt=[];wrong.forEach(function(d){if(d.topic&&wt.indexOf(d.topic)<0)wt.push(d.topic);}); wt.slice(0,3).forEach(function(t){ var sug=(r.weak||[]).filter(function(w){return w.topic===t;})[0]; var one=wrong.filter(function(d){return d.topic===t;})[0]; var tip=(sug&&sug.suggestion)||(one&&one.feedback)||'再练几道同类题就稳啦~'; lines.push({c:'var(--nova-red)',t:'📌 '+t+' 还不太稳——'+tip}); }); return lines; } function loadAnalysisReport(){ var box=document.getElementById('analysisContent');if(!box)return; var reports=loadReports(); var h=''; h+='
'; h+='
每次练习后自动留存,可回看、可重练错题
'; if(reports.length>0)h+=''; h+='
'; if(reports.length===0){ h+='
🔭
还没有练习小结~
完成一次练习,这里就会留下分析
'; box.innerHTML=h;return; } reports.slice(0,20).forEach(function(r){ var ds=r.details||[]; var wrongN=ds.filter(function(d){return !d.ok;}).length; var stars=ds.map(function(d){return d.ok?'⭐':'🌑';}).join(' '); var rid=r.id||''; h+='
'; h+='
'; h+='
'+(r.source==='exam'?'📝 试卷':'✏️ 练习')+' · '+(r.subjectName||'综合')+'
'; h+='
'+(r.dateLabel||'')+'
'; h+=''; h+='
'; h+='
'+stars+'
'; lightSummary(r).forEach(function(l){h+='
'+l.t+'
';}); h+=''; var disp=lightExpand[rid]?'block':'none'; h+='
'; if(lightExpand[rid]){ ds.forEach(function(d,i){ var ok=d.ok; h+='
'; h+='
'+(ok?'⭐':'🌑')+' 第'+(i+1)+'题 · '+(d.topic||'')+'
'; h+='
'+wEsc((d.question_text||'').substring(0,100))+'
'; h+='
你的答案:'+wEsc(d.student_answer||'未作答')+''+(ok?'':'正确答案:'+wEsc(d.correct_answer||'')+'')+'
'; if(!ok&&d.feedback)h+='
💡 '+wEsc(d.feedback)+'
'; if(!ok){h+='';h+='
';} h+='
'; }); } h+='
'; }); if(reports.length>20)h+='
仅显示最近20次,更早记录在学习档案里管理
'; box.innerHTML=h; } function toggleLightDetail(rid){lightExpand[rid]=!lightExpand[rid];loadAnalysisReport();} function deleteReport(id,fromArchive){ if(!confirm('删除这份报告?其中的错题也会一并移除~'))return; var arr=loadReports().filter(function(r){return r.id!==id;}); localStorage.setItem('practice_reports',JSON.stringify(arr)); toast('已删除'); loadAnalysisReport(); if(fromArchive)loadArchive(); loadErrors(); } function clearLightReports(){ if(!confirm('清空全部练习报告?试卷报告会保留,练习产生的错题也会一并清掉哦~'))return; var arr=loadReports().filter(function(r){return r.source==='exam';}); localStorage.setItem('practice_reports',JSON.stringify(arr)); toast('已清空'); loadAnalysisReport(); loadErrors(); } async function lightVariant(rid,di){ var box=document.getElementById('lv_'+rid+'_'+di);if(!box)return; if(!D.wrongItems){try{await loadErrors();}catch(e){}} var it=null;(D.wrongItems||[]).forEach(function(x){if(x.reportId===rid&&x.detailIndex===di)it=x;}); if(!it){box.innerHTML='
这道题的原记录已被清理~
';return;} if(!it.backendId){box.innerHTML='
这道题还没同步好,稍后再试~
';return;} box.innerHTML='
✨ 正在出一道同类题...
'; try{ var r=await fetch('/api/errors/'+encodeURIComponent(it.backendId)+'/variant',{method:'POST'}); var d=await r.json(); if(d.error){box.innerHTML='
出题失败了,稍后再试~
';return;} D.lightVar=D.lightVar||{}; D.lightVar[rid+'_'+di]={q:d.question||{},topic:d.topic||it.topic||'',ans:'',reveal:false}; lightVarRender(rid,di); }catch(e){box.innerHTML='
网络不太好,稍后再试~
';} } function lightVarRender(rid,di){ var box=document.getElementById('lv_'+rid+'_'+di);if(!box)return; var st=(D.lightVar||{})[rid+'_'+di];if(!st){box.innerHTML='';return;} var q=st.q;var hasOpt=q.type==='choice'&&q.options&&q.options.length; var h='
'; h+='
✨ 同类变式 · '+wEsc(st.topic||q.topic||'')+'
'; h+='
'+wEsc(q.question||'')+'
'; if(hasOpt){ h+='
'; (q.options||[]).forEach(function(o){var sel=st.ans===o.label;h+='';}); h+='
'; }else{ h+=''; } if(!st.reveal){ h+=''; }else{ var ok=hasOpt?((st.ans||'').toUpperCase()===(''+q.correct_answer).toUpperCase()):(wNorm(st.ans||'')===wNorm(''+q.correct_answer)); h+='
'+(ok?'✅ 太棒了,这个知识点稳啦!':'❌ 差一点点,正确答案:'+wEsc(''+q.correct_answer)+''+(q.explanation?'
💡 '+wEsc(q.explanation)+'
':''))+'
'; h+=''; } h+='
'; box.innerHTML=h; } function lightVarOpt(rid,di,label){var st=(D.lightVar||{})[rid+'_'+di];if(st){st.ans=label;st.reveal=false;}lightVarRender(rid,di);} function lightVarInput(rid,di,v){var st=(D.lightVar||{})[rid+'_'+di];if(st)st.ans=v;} function lightVarSubmit(rid,di){var st=(D.lightVar||{})[rid+'_'+di];if(st)st.reveal=true;lightVarRender(rid,di);} function lightVarClose(rid,di){if(D.lightVar)delete D.lightVar[rid+'_'+di];var box=document.getElementById('lv_'+rid+'_'+di);if(box)box.innerHTML='';} // ═══ 航行档案:错题星图(仅轻松练习,学科可折叠)+ 训练师星轨(形象图形)═══ async function loadArchive(){ var box=document.getElementById('archiveContent');if(!box)return; box.innerHTML='
🌌
正在点亮航行档案...
'; try{ var r=await fetch(API+'/profile');if(!r.ok){box.innerHTML='
数据加载失败~
';return;} var profile=await r.json(); var fp=profile.feedback_profile||{}; var rE=await fetch(API+'/errors?limit=500'); var errsAll=(rE.ok?(await rE.json()).errors||[]:[]).filter(function(e){return e.source_type==='practice';}); var subjN={math:'数学',chinese:'语文',english:'英语'}; var subjC={math:'var(--sirius)',chinese:'var(--nova-red)',english:'var(--nova-green)'}; var bySubj={math:[],chinese:[],english:[]}; errsAll.forEach(function(e){var s=e.subject;if(bySubj[s])bySubj[s].push(e);}); var totalErr=errsAll.length; var h=''; // ── 子板块1:错题星图(学科可折叠)── h+='
🪐 错题星图 仅收录「练题」中的错题
'; if(totalErr===0){ h+='
🌟
练题还没出现错题~ 做错一题就会在这里存档,方便你回头攻克
'; }else{ ['math','chinese','english'].forEach(function(s){ var arr=bySubj[s];if(!arr||arr.length===0)return; var subjOpen=localStorage.getItem('va_subj_'+s)==='1'; h+='
'; h+='
'; h+=''+subjN[s]+' · '+arr.length+' 道'+(subjOpen?'▴ 收起':'▾ 展开')+'
'; if(subjOpen){ h+='
'; arr.slice(0,40).forEach(function(e){ var eo=localStorage.getItem('va_exp_'+e.id)==='1'; h+='
'; h+='
'; h+='
🌑 '+wEsc(e.topic||'错题')+(e.error_type?(' · '+wEsc(e.error_type)):'')+'
'; h+='
'+wEsc((e.added_at||'').slice(0,10))+(eo?' ▴':' ▾')+'
'; if(eo){ h+='
'; h+='
题干:'+(e.question_text?wEsc(e.question_text):'')+'
'; h+='
正解:'+(e.correct_answer?wEsc(e.correct_answer):'—')+'
'; h+='
解析:'+(e.solution?wEsc(e.solution):'(暂未生成)')+'
'; h+='
'; } h+='
'; }); if(arr.length>40)h+='
仅显示最近 40 道,其余在练题中回顾
'; h+='
'; } h+='
'; }); } h+='
'; // ── 子板块2:训练师星轨(形象图形)── h+='
🎓 训练师星轨 你每反馈一次,AI 就更懂你一点
'; h+=vaTrainOrbit(fp); h+='
'; box.innerHTML=h; }catch(e){box.innerHTML='
加载失败~
';} } // 训练师星轨:形象图形(点亮星空 + 飞船沿星轨前进),不罗列数据 function starPath(cx,cy,r){ var p=[]; for(var i=0;i<10;i++){var a=-Math.PI/2+i*Math.PI/5;var rad=(i%2===0)?r:r*0.45;p.push((cx+Math.cos(a)*rad).toFixed(1)+','+(cy+Math.sin(a)*rad).toFixed(1));} return 'M'+p.join('L')+'Z'; } function vaTrainOrbit(fp){ var trainN=fp.training_count||0; var bias=parseFloat(fp.difficulty_bias||0)||0; var W=680,H=240,MAX=10; var litN=Math.min(trainN,MAX); var nodes=[]; for(var i=0;i';}else{stars+='';}} var si=Math.min(litN,MAX-1);var sp=nodes[si]; var aura=bias<-0.05?'#3FA7FF':(bias>0.05?'#FF7A45':'#8B7CFF'); var ship='🚀'; var hint=trainN===0?'点亮第一颗星,开始教 AI 出更贴你的题 🌟':(bias<-0.2?'AI 正把题出得更轻松 🌿':(bias>0.2?'AI 正把题出得更有挑战 🔥':'AI 正贴着你的节奏走 🌟')); var svg=''; svg+=''; svg+=''; svg+=''; svg+=stars+ship; svg+='训练师星轨'; svg+=''; return svg+'
'+hint+'
'; } // 错题星图 展开/收起 function vaToggle(id){ var k='va_exp_'+id; localStorage.setItem(k, localStorage.getItem(k)==='1'?'0':'1'); loadArchive(); } // 错题星图 学科折叠 function vaToggleSubj(s){ var k='va_subj_'+s; localStorage.setItem(k, localStorage.getItem(k)==='1'?'0':'1'); loadArchive(); } // 动态小结已移除:航行档案仅保留 错题星图 + 训练师星轨 // KB — redirect to upload view document.getElementById('btnKB').addEventListener('click',()=>openUpload('exam')); document.getElementById('btnKBUpload').addEventListener('click',uploadKB); document.getElementById('btnKBOCR').addEventListener('click',ocrUpload); async function loadKBStats(){try{const r=await fetch('/api/kb/stats');const d=await r.json();document.getElementById('kbStats').innerHTML='📊 星星总数:'+d.total_points+' · 📝 光谱记录:'+d.exercises_count+' 条';}catch(e){}} async function uploadKB(){const t=document.getElementById('kbText').value.trim();if(!t){document.getElementById('kbMsg').textContent='请先输入数据~';return;}document.getElementById('kbMsg').textContent='上传中...';try{const r=await fetch('/api/kb/upload?text='+encodeURIComponent(t)+'&subject=数学&chapter=自定义',{method:'POST'});const d=await r.json();if(d.status==='ok'){document.getElementById('kbText').value='';document.getElementById('kbMsg').textContent='✅ 星星已录入星图!';loadKBStats();}else{document.getElementById('kbMsg').textContent='❌ '+d.error;}}catch(e){document.getElementById('kbMsg').textContent='上传失败';}} function previewKB(){const f=document.getElementById('kbFile').files[0];if(!f)return;if(f.type.startsWith('image/')){const r=new FileReader();r.onload=e=>{document.getElementById('kbPreview').innerHTML='';};r.readAsDataURL(f);}else{document.getElementById('kbPreview').innerHTML=f.name;}} async function ocrUpload(){const f=document.getElementById('kbFile').files[0];if(!f){document.getElementById('kbMsg').textContent='请先选择文件';return;}document.getElementById('kbMsg').textContent='🔍 AI正在扫描...';try{const r=new FileReader();const b64=await new Promise((rs,rj)=>{r.onload=e=>rs(e.target.result.split(',')[1]);r.onerror=rj;r.readAsDataURL(f);});const form=new FormData();form.append('image_base64',b64);const resp=await fetch('/api/kb/ocr',{method:'POST',body:form});const d=await resp.json();if(d.error){document.getElementById('kbMsg').textContent='❌ '+d.error;return}const items=d.items||[];const text=items.map(i=>'Q: '+(i.question||'')+'\nA: '+(i.correct||i.answer||'')).join('\n---\n');document.getElementById('kbText').value=text||JSON.stringify(d);document.getElementById('kbMsg').textContent='✅ 识别到 '+items.length+' 颗星体!确认后点"录入星图"';}catch(e){document.getElementById('kbMsg').textContent='光谱分析失败,请手动输入';}} // UPLOAD let uploadFilesList=[]; let uploadOCRDone=false; let uploadOcrText=''; const UPLOAD_TYPE_LABELS={exam:'考卷',homework:'作业'}; const UPLOAD_TYPE_COLORS={exam:'background:#FAECE7;color:#F0997B',homework:'background:var(--sirius-lt);color:var(--sirius)'}; function switchUploadTab(tab){ document.getElementById('uploadType').value=tab; const isExam=tab==='exam'; var th=document.getElementById('tabHomework'),te=document.getElementById('tabExam'); if(th){th.style.background=isExam?'var(--card)':'linear-gradient(135deg,#8AB4F8,#5B7FDB)';th.style.color=isExam?'var(--sub)':'#fff';} if(te){te.style.background=isExam?'linear-gradient(135deg,#FFD740,#FFA000)':'var(--card)';te.style.color=isExam?'var(--void)':'var(--sub)';} document.getElementById('uploadZoneLabel').textContent=isExam?'每张拍一页,整卷一起传':'只拍做错和不会的题'; document.getElementById('uploadZoneHint').textContent=isExam?'每张照片只拍一页。拍完一页可以再点这里拍下一页,把整张考卷的所有页加到一起,点「🚀 识别并上传」一次完成':'做对的作业不用拍!只拍错题和不会的题(1-2张就行),选好后点「🚀 识别并上传」'; document.querySelectorAll('.exam-only').forEach(el=>el.style.display=isExam?'':'none'); resetUploadForm(); // 上传记录只显示本栏目(传作业/传试卷)的内容 try{loadUploadHistory();}catch(e){} } function resetUploadForm(){ uploadFilesList=[]; uploadOCRDone=false; uploadOcrText=''; document.getElementById('uploadFiles').value=''; document.getElementById('uploadPreview').innerHTML=''; document.getElementById('uploadOcrResult').innerHTML=''; document.getElementById('btnUploadOCR').style.display='none'; document.getElementById('btnUploadSubmit').style.display='none'; var up=document.getElementById('uploadProgress');if(up)up.style.display='none'; document.getElementById('uploadMsg').textContent=''; } function handleUploadFiles(files){ if(!files||files.length===0) return; // 追加而不是清空:拍完一页可以再点拍摄区加下一页,整卷拍完一起上传 uploadOcrText=''; uploadOCRDone=false; const maxFiles=12; Array.from(files).forEach(f=>{ if(uploadFilesList.length0){ document.getElementById('btnUploadSubmit').style.display='block'; document.getElementById('btnUploadOCR').style.display='block'; } } function renderUploadPreview(){ const container=document.getElementById('uploadPreview'); if(uploadFilesList.length===0){container.innerHTML='';return;} container.innerHTML='
已拍 '+uploadFilesList.length+' 页 · 点上方拍摄区可继续加页,整卷拍完一起上传 清空重选
'; const imgRow=document.createElement('div'); imgRow.style.cssText='display:flex;gap:8px;flex-wrap:wrap'; uploadFilesList.forEach((f,i)=>{ const div=document.createElement('div'); div.style.cssText='position:relative;width:70px;height:70px;border-radius:10px;overflow:hidden;border:1px solid var(--border)'; if(f.type.startsWith('image/')){ const reader=new FileReader(); reader.onload=e=>{const img=document.createElement('img');img.src=e.target.result;img.style.cssText='width:100%;height:100%;object-fit:cover';div.innerHTML='';div.appendChild(img);}; reader.readAsDataURL(f); }else{ div.innerHTML='
PDF
'; } const close=document.createElement('div'); close.textContent='×'; close.style.cssText='position:absolute;top:2px;right:4px;font-size:14px;color:#fff;background:rgba(0,0,0,0.5);border-radius:50%;width:18px;height:18px;display:flex;align-items:center;justify-content:center;cursor:pointer'; close.onclick=()=>{uploadFilesList.splice(i,1);renderUploadPreview();if(uploadFilesList.length===0){document.getElementById('btnUploadOCR').style.display='none';document.getElementById('btnUploadSubmit').style.display='none';var up=document.getElementById('uploadProgress');if(up)up.style.display='none';}}; div.appendChild(close); imgRow.appendChild(div); }); container.appendChild(imgRow); } // ── 图片压缩(Canvas 缩放至 1920px,JPEG quality 0.85)── function compressImage(file){ return new Promise(function(resolve,reject){ var reader=new FileReader(); reader.onload=function(e){ var img=new Image(); img.onload=function(){ var maxW=1920,maxH=1920; var w=img.width,h=img.height; if(w>maxW||h>maxH){ var scale=Math.min(maxW/w,maxH/h); w=Math.round(w*scale);h=Math.round(h*scale); } var canvas=document.createElement('canvas'); canvas.width=w;canvas.height=h; var ctx=canvas.getContext('2d'); ctx.fillStyle='#fff'; ctx.fillRect(0,0,w,h); ctx.drawImage(img,0,0,w,h); var dataUrl=canvas.toDataURL('image/jpeg',0.85); resolve(dataUrl.split(',')[1]); }; img.onerror=reject; img.src=e.target.result; }; reader.onerror=reject; reader.readAsDataURL(file); }); } // ── 并行 OCR(多张图同时识别,大幅提速)── async function parallelOCR(onProgress){ var total=uploadFilesList.length; var allItems=[]; var failedPages=[]; var done=0; var promises=uploadFilesList.map(function(f,idx){ return new Promise(function(resolve){ compressImage(f).then(async function(b64){ try{ var form=new FormData(); form.append('image_base64',b64); form.append('mark_mode',isMarkMode()?'1':'0'); var resp=await fetch('/api/kb/ocr',{method:'POST',body:form}); var d=await resp.json(); done++; if(onProgress)onProgress(done,total); if(d.error){failedPages.push(idx+1);resolve(null);return;} var items=d.items||[]; items.forEach(function(it){it.__page=idx+1;}); resolve(items); }catch(err){ done++; if(onProgress)onProgress(done,total); failedPages.push(idx+1); resolve(null); } }).catch(function(){ done++; if(onProgress)onProgress(done,total); failedPages.push(idx+1); resolve(null); }); }); }); var results=await Promise.all(promises); results.forEach(function(items){ if(items)allItems=allItems.concat(items); }); return {allItems:allItems,failedPages:failedPages,total:total}; } // ── OCR 结果格式化 ── function formatOCRText(allItems){ return allItems.map(function(i){ var s='【第 '+(i.__page||1)+' 页】\nQ: '+(i.question||''); if(i.answer)s+='\n学生答案: '+i.answer; else if(i.blank)s+='\n未作答'; if(i.student_mark)s+='\n🔴 自标▲(有问题)'; if(i.teacher_mark==='wrong')s+='\n老师批改: 红叉(错)'; else if(i.correct)s+='\n老师批改: '+i.correct+(i.marked_wrong?'(错)':''); return s; }).join('\n---\n'); } // 红笔▲标记模式:读取上次选择,默认关闭 (function(){ try{ var _mm=document.getElementById('markMode'); if(_mm)_mm.checked=(localStorage.getItem('markMode')==='1'); }catch(e){} })(); function isMarkMode(){ var _mm=document.getElementById('markMode'); return !!(_mm&&_mm.checked); } // ── 一键识别并上传(核心改进)── var _uploading=false; async function oneClickUpload(){ if(_uploading)return; // 防重复提交 if(uploadFilesList.length===0){document.getElementById('uploadMsg').textContent='请先选择文件';return;} _uploading=true; var btnSubmit=document.getElementById('btnUploadSubmit'); var btnOCR=document.getElementById('btnUploadOCR'); var progBox=document.getElementById('uploadProgress'); var progBar=document.getElementById('uploadProgBar'); var progText=document.getElementById('uploadProgText'); var msg=document.getElementById('uploadMsg'); btnSubmit.disabled=true; btnOCR.disabled=true; btnSubmit.textContent='⏳ 处理中...'; btnOCR.style.display='none'; progBox.style.display='block'; msg.textContent=''; var type=document.getElementById('uploadType').value; var subject=document.getElementById('uploadSubject').value; var examType=document.getElementById('uploadExamType').value; var title=(document.getElementById('uploadTitle').value.trim())||(type==='exam'?subject+examType:subject+'作业'); // Step 0: 同卷防呆——48小时内同科目同标题已存在,优先补页而不是新建 var dup=await findDupUpload(subject,title); if(dup){ var mergeOk=confirm('检测到《'+title+'》在 '+(String(dup.created_at||'').slice(5,16))+' 已上传过('+(dup.file_count||1)+'页)。\n\n如果是同一份卷子补拍的页:点「确定」把本次照片追加为新页;\n如果确实是另一份新资料:点「取消」继续新建。'); if(mergeOk){ progBox.style.display='block'; progBar.style.width='40%'; progText.textContent='📤 正在追加新页并重新识别整卷...'; msg.textContent='📤 追加页中,AI 重新识别整卷...'; var ad3=await appendUploadFiles(dup.id,uploadFilesList); if(ad3&&ad3.ok){ var rc3=ad3.reocr||{}; if(rc3.errors_added&&rc3.errors_added.length){syncUploadErrors(rc3.errors_added);} progBar.style.width='100%'; progText.textContent='✅ 完成!'; msg.textContent='✅ 已追加到《'+title+'》(共'+ad3.total_pages+'页),重新识别'+(rc3.items_count||0)+'题'; uploadFilesList=[]; document.getElementById('uploadFiles').value=''; document.getElementById('uploadPreview').innerHTML=''; btnSubmit.style.display='none'; btnOCR.style.display='none'; btnSubmit.disabled=false; btnSubmit.textContent='🚀 识别并上传'; btnOCR.disabled=false; setTimeout(function(){progBox.style.display='none';},2000); _uploading=false; loadUploadHistory(); loadErrors(); try{}catch(e){} addDailyPts(2,'拍了错题'); return; } // 追加失败则回落到正常新建流程 progBox.style.display='none'; msg.textContent='⚠️ 追加失败('+(((ad3&&ad3.error)||'网络错误'))+'),改为新建上传'; } } // Step 1: 并行 OCR progBar.style.width='10%'; progText.textContent='🔍 正在识别 '+uploadFilesList.length+' 页...'; msg.textContent='🔍 AI 识别中...'; var ocrResult; try{ ocrResult=await parallelOCR(function(done,total){ progBar.style.width=(10+Math.round(done/total*50))+'%'; progText.textContent='🔍 识别进度:'+done+'/'+total+' 页'; }); }catch(e){ msg.textContent='❌ 识别失败,请重试'; btnSubmit.disabled=false;btnOCR.disabled=false; btnSubmit.textContent='🚀 识别并上传';btnOCR.style.display='block'; progBox.style.display='none';_uploading=false; return; } var allItems=ocrResult.allItems; var failedPages=ocrResult.failedPages; var total=ocrResult.total; if(allItems.length===0){ var why=failedPages.length?('第 '+failedPages.join('、')+' 页识别失败'):'未识别到题目内容'; msg.textContent='❌ '+why+'(请重拍或检查清晰度)'; btnSubmit.disabled=false;btnOCR.disabled=false; btnSubmit.textContent='🚀 识别并上传';btnOCR.style.display='block'; progBox.style.display='none';_uploading=false; return; } uploadOcrText=formatOCRText(allItems); window._lastOcrItems=allItems; // 结构化识别结果(含blank/teacher_mark),上传时传给后端判定 uploadOCRDone=true; // 显示识别结果 document.getElementById('uploadOcrResult').innerHTML='
'+uploadOcrText+'
'; // Step 2: AI 缺漏分析 progBar.style.width='65%'; progText.textContent='🧠 AI 正在分析知识缺漏...'; msg.textContent='🧠 分析缺漏中...'; var analysis={summary:'',weak_points:[]}; if(allItems.length>0){ try{ var af=new FormData(); af.append('subject',subject); af.append('material_type',type); af.append('ocr_text',uploadOcrText||''); af.append('ocr_items',JSON.stringify(allItems.map(function(it){ return {question:it.question||'',answer:it.answer||'',correct:it.correct||'',marked_wrong:it.marked_wrong||false,blank:!!it.blank,teacher_mark:it.teacher_mark||'none',student_mark:!!it.student_mark,page:it.__page||1}; }))); var ar=await fetch('/api/student/analyze',{method:'POST',body:af}); var ad=await ar.json(); analysis={summary:ad.summary||'',weak_points:ad.weak_points||[]}; }catch(e){} } // Step 3: 上传 progBar.style.width='80%'; progText.textContent='📤 正在上传...'; msg.textContent='📤 上传中...'; var apiOk=false; try{ var formData=new FormData(); uploadFilesList.forEach(function(f){formData.append('files',f);}); formData.append('upload_type',type); formData.append('subject',subject); formData.append('exam_type',type==='exam'?examType:''); formData.append('title',title); formData.append('ocr_text',uploadOcrText||''); formData.append('ocr_items',JSON.stringify((window._lastOcrItems||[]).map(function(it){ return {question:it.question||'',answer:it.answer||'',correct:it.correct||'',marked_wrong:it.marked_wrong||false,blank:!!it.blank,teacher_mark:it.teacher_mark||'none',student_mark:!!it.student_mark,page:it.__page||1}; }))); formData.append('mark_mode',isMarkMode()?'1':'0'); formData.append('analysis',JSON.stringify(analysis)); var r=await fetch('/api/student/upload',{method:'POST',body:formData}); if(r.ok){ var d=await r.json(); if(d.ok)apiOk=true; if(d.errors_added&&d.errors_added.length){syncUploadErrors(d.errors_added);} } }catch(e){} // fallback if(!apiOk){ try{ var text=uploadOcrText||''; if(text){ var r2=await fetch('/api/kb/upload?text='+encodeURIComponent(text)+'&subject='+encodeURIComponent(subject)+'&chapter='+encodeURIComponent(type==='exam'?examType:'作业'),{method:'POST'}); if(r2.ok)apiOk=true; } }catch(e){} } // Step 4: 存本地历史 var hist=JSON.parse(localStorage.getItem('upload_history')||'[]'); var entry={id:Date.now(),title:title,type:type,subject:subject,exam_type:type==='exam'?examType:'',file_count:uploadFilesList.length,ocr_done:uploadOCRDone,ocr_text:uploadOcrText,analysis:analysis,created_at:new Date().toISOString(),synced:apiOk,file_paths:[]}; hist.unshift(entry); localStorage.setItem('upload_history',JSON.stringify(hist.slice(0,50))); // 展示分析 renderUploadAnalysis(analysis); // 完成 progBar.style.width='100%'; progText.textContent='✅ 完成!'; var succMsg='✅ 已上传'+(apiOk?'':'(本地保存)')+',识别 '+allItems.length+' 题'; if(failedPages.length)succMsg+='('+failedPages.length+' 页失败)'; if(analysis.weak_points&&analysis.weak_points.length)succMsg+=',发现 '+analysis.weak_points.length+' 个薄弱点'; msg.textContent=succMsg; addDailyPts(2,'拍了错题'); // 清理 uploadFilesList=[]; document.getElementById('uploadFiles').value=''; document.getElementById('uploadPreview').innerHTML=''; btnSubmit.style.display='none'; btnOCR.style.display='none'; btnSubmit.disabled=false; btnSubmit.textContent='🚀 识别并上传'; btnOCR.disabled=false; _uploading=false; loadUploadHistory(); // 自动返回日常学习页并刷新 setTimeout(function(){ showView('daily'); // 刷新日常学习数据 renderDaily(); toast('✅ 上传成功!'+allItems.length+' 道题已入库'); },2000); } async function doUploadOCR(){ if(uploadFilesList.length===0) return; document.getElementById('uploadMsg').textContent='🔍 AI 正在识别...'; document.getElementById('btnUploadOCR').disabled=true; try{ const total=uploadFilesList.length; var ocrResult=await parallelOCR(function(done,tot){ document.getElementById('uploadMsg').textContent='🔍 识别进度:'+done+'/'+tot+' 页'; }); var allItems=ocrResult.allItems; var failedPages=ocrResult.failedPages; if(allItems.length===0){ const why=failedPages.length?('第 '+failedPages.join('、')+' 页识别失败'):'未识别到题目内容'; document.getElementById('uploadMsg').textContent='❌ '+why+'(请重试或检查图片清晰度)'; document.getElementById('uploadOcrResult').innerHTML=''; uploadOCRDone=false; uploadOcrText=''; return; } uploadOcrText=allItems.map(function(i){ let s='【第 '+(i.__page||1)+' 页】\nQ: '+(i.question||''); if(i.answer)s+='\n学生答案: '+i.answer; else if(i.blank)s+='\n未作答'; if(i.student_mark)s+='\n🔴 自标▲(有问题)'; if(i.teacher_mark==='wrong')s+='\n老师批改: 红叉(错)'; else if(i.correct)s+='\n老师批改: '+i.correct+(i.marked_wrong?'(错)':''); return s; }).join('\n---\n'); uploadOCRDone=true; document.getElementById('uploadOcrResult').innerHTML='
'+uploadOcrText+'
'; if(failedPages.length){ document.getElementById('uploadMsg').textContent='⚠️ 第 '+failedPages.join('、')+' 页识别失败,已采用其余 '+(total-failedPages.length)+' 页,共识别 '+allItems.length+' 题,点上方「🚀 识别并上传」一键完成'; }else{ document.getElementById('uploadMsg').textContent='✅ 识别到 '+allItems.length+' 道题(共 '+total+' 页)!点上方「🚀 识别并上传」一键完成'; } }catch(e){ const msg=(e&&e.message)?e.message:String(e); document.getElementById('uploadMsg').textContent='❌ 识别失败:'+msg+'(请重试或检查网络)'; document.getElementById('uploadOcrResult').innerHTML=''; uploadOCRDone=false; uploadOcrText=''; }finally{ document.getElementById('btnUploadOCR').disabled=false; } } async function submitUpload(){ if(_uploading)return; // 防重复提交 if(uploadFilesList.length===0){document.getElementById('uploadMsg').textContent='请先选择文件';return;} _uploading=true; const type=document.getElementById('uploadType').value; const subject=document.getElementById('uploadSubject').value; const examType=document.getElementById('uploadExamType').value; const title=document.getElementById('uploadTitle').value.trim()||(type==='exam'?subject+examType:subject+'作业'); document.getElementById('uploadMsg').textContent='📤 上传并分析缺漏中...'; document.getElementById('btnUploadSubmit').disabled=true; // 1) AI 知识缺漏分析(老师已批改,只识别薄弱点,不批改) let analysis={summary:'',weak_points:[]}; if(uploadOcrText&&uploadOcrText.trim()){ try{ const af=new FormData(); af.append('subject',subject); af.append('material_type',type); af.append('ocr_text',uploadOcrText); const ar=await fetch('/api/student/analyze',{method:'POST',body:af}); const ad=await ar.json(); analysis={summary:ad.summary||'',weak_points:ad.weak_points||[]}; }catch(e){} } // 2) 上传(带 analysis) let apiOk=false; try{ const formData=new FormData(); uploadFilesList.forEach(f=>formData.append('files',f)); formData.append('upload_type',type); formData.append('subject',subject); formData.append('exam_type',type==='exam'?examType:''); formData.append('title',title); formData.append('ocr_text',uploadOcrText||''); formData.append('analysis',JSON.stringify(analysis)); const r=await fetch('/api/student/upload',{method:'POST',body:formData}); if(r.ok){const d=await r.json();if(d.ok) apiOk=true; if(d.errors_added&&d.errors_added.length){syncUploadErrors(d.errors_added);} if(d.correct_topics&&d.correct_topics.length){try{document.getElementById('uploadMsg').textContent='✅ '+d.correct_topics.length+' 个做对的知识点已登记';}catch(e){}}} }catch(e){} if(!apiOk){ try{ const text=uploadOcrText||''; if(text){ const r=await fetch('/api/kb/upload?text='+encodeURIComponent(text)+'&subject='+encodeURIComponent(subject)+'&chapter='+encodeURIComponent(type==='exam'?examType:'作业'),{method:'POST'}); if(r.ok) apiOk=true; } }catch(e){} } // 3) 存本地历史(带 analysis) const hist=JSON.parse(localStorage.getItem('upload_history')||'[]'); const entry={id:Date.now(),title:title,type:type,subject:subject,exam_type:type==='exam'?examType:'',file_count:uploadFilesList.length,ocr_done:uploadOCRDone,ocr_text:uploadOcrText,analysis:analysis,created_at:new Date().toISOString(),synced:apiOk,file_paths:[]}; hist.unshift(entry); localStorage.setItem('upload_history',JSON.stringify(hist.slice(0,50))); // 4) 展示缺漏分析 + 清理文件区(保留分析与识别结果供查看) renderUploadAnalysis(analysis); uploadFilesList=[]; document.getElementById('uploadFiles').value=''; document.getElementById('uploadPreview').innerHTML=''; document.getElementById('btnUploadOCR').style.display='none'; document.getElementById('btnUploadSubmit').style.display='none'; document.getElementById('uploadMsg').textContent='✅ 已上传'+(apiOk?'':'(本地保存)')+',缺漏分析见下方'; document.getElementById('btnUploadSubmit').disabled=false; _uploading=false; loadUploadHistory(); // 自动返回日常学习页并刷新 setTimeout(function(){ showView('daily'); renderDaily(); toast('✅ 上传成功!'); },2000); } let UPLOAD_DETAIL={}; // uid -> {subject, topics} 供「针对性练习」查询 function parseAnalysis(a){ if(!a)return {summary:'',weak_points:[]}; if(typeof a==='object')return a; try{return JSON.parse(a);}catch(e){return {summary:'',weak_points:[]};} } function renderUploadAnalysis(a){ const box=document.getElementById('uploadAnalysis');if(!box)return; const wp=(a&&a.weak_points)||[]; let h='
'; h+='
🔍 知识缺漏分析
'; if(a&&a.summary)h+='
'+examEsc(a.summary)+'
'; if(wp.length===0){h+='
暂未识别出明显缺漏(可能识别文本不足,或本次全对)。
';} else{ wp.forEach(function(w){ const sev=w.severity==='高'?'color:var(--nova-red)':(w.severity==='中'?'color:var(--sol)':'color:var(--nova-green)'); h+='
📌 '+examEsc(w.topic||'薄弱点')+' '+examEsc(w.severity||'中')+' '+examEsc(w.error_type||'')+'
'; h+='
💡 '+examEsc(w.suggestion||'')+'
'; }); } h+='
'; box.innerHTML=h; } function toggleUploadGroup(gid){ const el=document.getElementById(gid);const arr=document.getElementById(gid+'_arrow');if(!el)return; if(el.style.display==='none'){el.style.display='block';if(arr)arr.textContent='▾';} else{el.style.display='none';if(arr)arr.textContent='▸';} } function toggleUploadDetail(uid){ const el=document.getElementById(uid);const arr=document.getElementById(uid+'_arrow');if(!el)return; if(el.style.display==='none'){el.style.display='block';if(arr)arr.textContent='▾';} else{el.style.display='none';if(arr)arr.textContent='▸';} } function uploadHistoryItem(h){ const typeLabel=UPLOAD_TYPE_LABELS[h.type]||h.type; const typeColor=UPLOAD_TYPE_COLORS[h.type]||UPLOAD_TYPE_COLORS.exam; const date=h.created_at?h.created_at.slice(5,16).replace('T',' '):''; const status=h.ocr_done?'已识别':'待识别'; const a=parseAnalysis(h.analysis); const hasWp=(a.weak_points&&a.weak_points.length>0); const uid='ud_'+h.id; UPLOAD_DETAIL[uid]={subject:h.subject,topics:(a.weak_points||[]).map(function(w){return w.topic;}).filter(Boolean),id:h.id,synced:h.synced,title:h.title}; let d='
'; d+='
'; d+=''+typeLabel+''; d+='
'+examEsc(h.title)+'
'; d+='
'+examEsc(h.subject)+' · '+(h.file_count||1)+'张 · '+date+(hasWp?' · 有缺漏':'')+'
'; d+='
'+status+' '+(h.synced?'':'⏳')+'
'; d+='
'; // ── 操作按钮行:始终可见(不藏在折叠区) ── d+='
'; d+=''; if(!h.ocr_done){d+='';} if(h.synced&&h.id){d+='';} d+=''; d+='
'; d+=''; d+='
'; return d; } async function loadUploadHistory(){ const container=document.getElementById('uploadHistory'); if(!container)return; var curTabEl=document.getElementById('uploadType'); var curTab=(curTabEl&&curTabEl.value)||'exam'; var wantType=curTab==='homework'?'homework':'exam'; var cacheKey='uhc_'+wantType; // 1) 秒开:先渲染本地缓存(解决“打开要等一会 / 像没存档”的错觉) var cache=[]; try{cache=JSON.parse(localStorage.getItem(cacheKey)||'[]');}catch(e){cache=[];} if(cache.length){ renderUploadList(container, cache, wantType); }else{ container.innerHTML='
⏳ 正在加载上传记录…
'; } // 2) 后台拉后端,刷新并写缓存 try{ const r=await fetch('/api/student/uploads?limit=60&type='+wantType); if(!r.ok)throw new Error('bad'); const d=await r.json(); var items=(d.items||[]).map(function(m){ let fps=[]; try{fps=JSON.parse(m.file_paths||'[]');}catch(e){fps=m.file_path?[m.file_path]:[];} return {id:m.id,title:m.title,type:m.material_type==='exam_paper'?'exam':'homework', subject:m.subject,exam_type:m.exam_type||'',file_count:m.file_count||1, ocr_done:m.ocr_status==='done',ocr_text:m.ocr_text||'', analysis:parseAnalysis(m.analysis),created_at:m.created_at||'',synced:true, file_type:m.file_type||'image',file_paths:fps}; }); var localHist=[]; try{localHist=JSON.parse(localStorage.getItem('upload_history')||'[]');}catch(e){} var localOnly=localHist.filter(function(l){return !l.synced;}); items=items.concat(localOnly); try{localStorage.setItem(cacheKey, JSON.stringify(items));}catch(e){} renderUploadList(container, items, wantType); }catch(e){ if(!cache.length){ container.innerHTML='
本栏目('+(wantType==='exam'?'试卷':'作业')+')还没有上传记录
'; }else{ renderUploadList(container, cache, wantType); } } } function renderUploadList(container, hist, wantType){ var scopeEl=document.getElementById('uploadHistoryScope'); if(scopeEl)scopeEl.textContent='只显示本栏目('+(wantType==='exam'?'试卷':'作业')+')上传的资料 · 按科目分组'; if(!hist||hist.length===0){container.innerHTML='
本栏目('+(wantType==='exam'?'试卷':'作业')+')还没有上传记录
';return;} const groups={}; hist.forEach(function(h){const s=matGroupKey(h.subject);(groups[s]=groups[s]||[]).push(h);}); let hh=''; Object.keys(groups).forEach(function(sub){ const items=groups[sub]; const gid='ug_'+String(sub); hh+=`
` +'
📂 '+examEsc(sub)+' ('+items.length+'份)
' +'
'; hh+='
'; items.forEach(function(h){hh+=uploadHistoryItem(h);}); hh+='
'; }); container.innerHTML=hh; } async function startTargetedPracticeById(uid){ const info=UPLOAD_DETAIL[uid]; if(!info)return; await startTargetedPractice(info.subject, info.topics); } // ── 补页:同一份试卷漏拍的页直接追加到已有资料,不新建 ── function clearUploadFiles(){ uploadFilesList=[]; uploadOcrText=''; uploadOCRDone=false; document.getElementById('uploadFiles').value=''; renderUploadPreview(); document.getElementById('btnUploadSubmit').style.display='none'; document.getElementById('btnUploadOCR').style.display='none'; var up=document.getElementById('uploadProgress');if(up)up.style.display='none'; } // 48小时内同科目同标题是否已有上传(防同卷拆分) async function findDupUpload(subject,title){ try{ var r=await fetch('/api/student/uploads?limit=30'); if(!r.ok)return null; var d=await r.json(); var now=Date.now(); var list=(d.items||[]).filter(function(m){ if((m.title||'')!==title)return false; if((m.subject||'')!==subject)return false; var t=Date.parse(String(m.created_at||'').replace(' ','T')); if(isNaN(t))return false; return (now-t)<48*3600*1000; }); return list.length?list[list.length-1]:null; }catch(e){return null;} } // 调后端append端点:追加页文件,后端重做整卷OCR并幂等重建错题 async function appendUploadFiles(matId,files){ try{ var fd=new FormData(); Array.from(files).forEach(function(f){fd.append('files',f);}); var r=await fetch('/api/student/uploads/'+matId+'/append',{method:'POST',body:fd}); return await r.json(); }catch(e){return {ok:false,error:'网络错误,请重试'};} } var _appendTargetId=0; function appendPagesClick(matId){ _appendTargetId=matId; var inp=document.getElementById('appendFilesInput'); if(!inp){ inp=document.createElement('input'); inp.type='file';inp.id='appendFilesInput'; inp.accept='image/*,.pdf';inp.multiple=true;inp.style.display='none'; inp.onchange=function(){doAppendPages(this.files);this.value='';}; document.body.appendChild(inp); } inp.click(); } async function doAppendPages(files){ if(!files||files.length===0)return; if(!confirm('把选中的 '+files.length+' 页追加到这份资料?AI 会重新识别整卷并更新错题。'))return; toast('📤 正在追加页并重新识别整卷,请稍候...'); var d=await appendUploadFiles(_appendTargetId,files); if(d&&d.ok){ var rc=d.reocr||{}; toast('✅ 已追加'+d.appended+'页(共'+d.total_pages+'页),识别'+(rc.items_count||0)+'题'); if(rc.errors_added&&rc.errors_added.length){syncUploadErrors(rc.errors_added);} loadUploadHistory();loadErrors();try{}catch(e){} }else{ toast('❌ '+(((d&&d.error)||'追加失败,请重试'))); } } async function reocrUpload(matId){ if(!confirm('确定重新识别这份资料吗?AI将重新分析图片内容。'))return; var btn=event.target; btn.disabled=true; btn.textContent='⏳ 识别中...'; try{ var r=await fetch('/api/student/uploads/'+matId+'/reocr',{method:'POST'}); var d=await r.json(); if(d.ok){ toast('✅ 识别成功!'+d.items_count+' 道题'); loadUploadHistory(); loadErrors(); }else{ toast('❌ '+(d.error||'识别失败,请重试')); btn.disabled=false; btn.textContent='🔍 重新识别'; } }catch(e){ toast('❌ 网络错误,请重试'); btn.disabled=false; btn.textContent='🔍 重新识别'; } } async function deleteUploadById(uid){ const info=UPLOAD_DETAIL[uid]; if(!info)return; if(!confirm('确定删除这条上传记录吗?测试或作废的记录可随时清理。'))return; if(info.synced&&info.id){ try{await fetch('/api/student/uploads/'+info.id,{method:'DELETE'});}catch(e){} } const hist=JSON.parse(localStorage.getItem('upload_history')||'[]'); const kept=hist.filter(function(l){ if(info.synced){return !(l.synced&&l.title===info.title);} return String(l.id)!==String(info.id); }); localStorage.setItem('upload_history',JSON.stringify(kept)); delete UPLOAD_DETAIL[uid]; // 同步清本地缓存,删除即生效(无需等后端刷新) try{ ['uhc_homework','uhc_exam'].forEach(function(k){ try{ var arr=JSON.parse(localStorage.getItem(k)||'[]'); var nk=arr.filter(function(x){return String(x.id)!==String(info.id);}); localStorage.setItem(k, JSON.stringify(nk)); }catch(e){} }); }catch(e){} loadUploadHistory(); loadErrors(); toast('🗑 已删除,关联错题已同步清理'); } async function startTargetedPractice(subject, topics){ showView('explore'); document.getElementById('btnNext').classList.remove('hidden'); document.getElementById('btnFinish').classList.add('hidden'); document.getElementById('progFill').style.width='0%'; document.getElementById('progHint').textContent='🎯 正在按缺漏知识点出题...'; try{ const form=new FormData(); form.append('subject',subject); form.append('topics',(topics||[]).join(',')); form.append('count','5'); const r=await fetch('/api/student/practice',{method:'POST',body:form}); const d=await r.json(); D.questions=d.questions||[]; D.answers={};D.curQ=0; if(D.questions.length===0){toast('题库暂无匹配题目,稍后再试~');showView('home');return;} if(!D.settings)D.settings={}; D.settings.subject={数学:'math',语文:'chinese',英语:'english'}[subject]||'math'; document.getElementById('progHint').textContent='🎯 针对性强化练习开始!'; updateProg();renderQ(); }catch(e){toast('出题失败,刷新再来~');} } // Bindings + Init initQA();initStarPicker(); document.getElementById('btnStart').addEventListener('click',practiceEntry); document.getElementById('btnNext').addEventListener('click',nextQ); document.getElementById('btnPrev').addEventListener('click',prevQ); document.getElementById('btnFinish').addEventListener('click',finish); document.getElementById('btnHome').addEventListener('click',()=>showView('home')); document.getElementById('btnRetry').addEventListener('click',startExplore); document.querySelectorAll('.nav-item').forEach(b=>b.addEventListener('click',()=>{if(b.dataset.v==='explore')practiceEntry();else showView(b.dataset.v);})); loadAvatar();showView('home'); // 练题前置:先提示去「我的」设置,再开始出题 function practiceEntry(){showView('setup');} document.getElementById('btnGoSettings').addEventListener('click',()=>{D.fromPracticePrompt=true;showView('settings');}); document.getElementById('btnSkipSetup').addEventListener('click',startExplore);