@arcaneorion/dsh-teaching-board 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -4
- package/cordis.patch.yml +6 -2
- package/package.json +5 -3
- package/skills/stage-panel/SKILL.md +107 -0
- package/skills/stage-panel/references/diagram-selection.md +80 -0
- package/skills/stage-panel/references/html-patterns.md +93 -0
- package/skills/stage-panel/references/visual-style.md +43 -0
- package/skills/stage-panel/submode/code/SKILL.md +17 -0
- package/skills/stage-panel/submode/code/examples/go-hello-channel.json +36 -0
- package/skills/stage-panel/submode/code/examples/go-http-api-basics.json +58 -0
- package/skills/stage-panel/submode/code/examples/py-agent-loop.json +79 -0
- package/skills/stage-panel/submode/code/examples/py-ai-parameters-intro.json +65 -0
- package/skills/stage-panel/submode/code/examples/py-fizzbuzz-intro.json +74 -0
- package/skills/stage-panel/submode/code/examples/py-training-loop.json +114 -0
- package/skills/stage-panel/submode/code/examples/rs-move-basics.json +118 -0
- package/skills/stage-panel/submode/code/references/host-toolchains.md +61 -0
- package/skills/stage-panel/submode/code/schemas/lesson.schema.json +225 -0
- package/skills/stage-panel/submode/code/templates/lab.html +561 -0
- package/skills/stage-panel/submode/game-study/SKILL.md +16 -0
- package/skills/stage-panel/submode/game-study/examples/hft-microstructure.json +60 -0
- package/skills/stage-panel/submode/game-study/examples/securities-law.json +57 -0
- package/skills/stage-panel/submode/game-study/schemas/quest.schema.json +95 -0
- package/skills/stage-panel/submode/game-study/templates/quiz.html +516 -0
- package/skills/stage-panel/submode/learn/SKILL.md +16 -0
- package/skills/stage-panel/submode/learn/examples/epsilon-delta.json +140 -0
- package/skills/stage-panel/submode/learn/examples/template-showcase.json +147 -0
- package/skills/stage-panel/submode/learn/schemas/lesson.schema.json +184 -0
- package/skills/stage-panel/submode/learn/templates/lesson.html +782 -0
- package/skills/stage-panel/submode/ml/SKILL.md +9 -0
- package/skills/stage-panel/submode/ml/examples/gd.json +7 -0
- package/skills/stage-panel/submode/ml/templates/gd.html +217 -0
- package/skills/stage-panel/submode/quant/SKILL.md +9 -0
- package/skills/stage-panel/submode/quant/references/export_backtest_data.py +192 -0
- package/skills/stage-panel/submode/quant/templates/dashboard.html +381 -0
- package/src/client.js +177 -15
- package/src/index.js +11 -0
- package/src/skills.js +88 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>LocalWeb Study · 自适应学习</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--line:#07111b; --base:#244960; --deck:#1f3e54; --panel:#1b3a4f;
|
|
10
|
+
--rail:#2e5266; --yellow:#ffd43b; --cream:#fff8d6; --orange:#ffb454;
|
|
11
|
+
--cyan:#35d8ff; --red:#ff6b6b; --green:#69db7c; --muted:#9fc0d0;
|
|
12
|
+
}
|
|
13
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
14
|
+
html,body{height:100%}
|
|
15
|
+
body{
|
|
16
|
+
font-family:'Cascadia Code','JetBrains Mono','Fira Code',Consolas,monospace;
|
|
17
|
+
background:var(--base);color:var(--cream);overflow:hidden;font-size:14px;
|
|
18
|
+
}
|
|
19
|
+
/* ===== 顶栏 ===== */
|
|
20
|
+
.topbar{display:flex;align-items:center;gap:14px;padding:9px 14px;background:var(--deck);border-bottom:3px solid var(--line)}
|
|
21
|
+
.topbar .domain{background:var(--line);color:var(--cyan);padding:2px 8px;border-radius:3px;font-size:11px;font-weight:700;white-space:nowrap}
|
|
22
|
+
.topbar .title{font-weight:800;color:var(--yellow);font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
|
|
23
|
+
.seg{display:flex;gap:0;border:2px solid var(--line);border-radius:4px;overflow:hidden}
|
|
24
|
+
.seg button{background:var(--panel);color:var(--muted);border:0;padding:5px 12px;font-family:inherit;font-size:12px;font-weight:700;cursor:pointer}
|
|
25
|
+
.seg button.active{background:var(--yellow);color:var(--line)}
|
|
26
|
+
.progress-wrap{display:flex;align-items:center;gap:7px;font-size:11px}
|
|
27
|
+
.pbar{width:96px;height:11px;background:var(--line);border:1px solid var(--line);border-radius:2px;overflow:hidden}
|
|
28
|
+
.pfill{height:100%;background:linear-gradient(90deg,var(--cyan),var(--green));transition:width .3s}
|
|
29
|
+
.stat-pill{background:var(--line);color:var(--cream);padding:2px 7px;border-radius:9px;font-size:11px;white-space:nowrap}
|
|
30
|
+
.stat-pill.warn{color:var(--orange)} .stat-pill.good{color:var(--green)} .stat-pill.bad{color:var(--red)}
|
|
31
|
+
/* ===== 视图容器 ===== */
|
|
32
|
+
.view{display:none;height:calc(100% - 42px);overflow-y:auto}
|
|
33
|
+
.view.active{display:block}
|
|
34
|
+
/* ===== 答题视图 ===== */
|
|
35
|
+
.quiz-stage{max-width:760px;margin:0 auto;padding:26px 22px 60px}
|
|
36
|
+
.qhead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px;color:var(--muted);font-size:12px}
|
|
37
|
+
.qhead .idx{color:var(--cyan);font-weight:700}
|
|
38
|
+
.qtopic{font-size:11px;color:var(--orange);margin-bottom:14px}
|
|
39
|
+
.qtopic .chip{background:var(--panel);border:1px solid var(--rail);border-radius:3px;padding:1px 6px;margin-right:4px}
|
|
40
|
+
.prompt{font-size:18px;line-height:1.6;color:var(--cream);font-weight:600;margin-bottom:20px}
|
|
41
|
+
.options{display:flex;flex-direction:column;gap:9px}
|
|
42
|
+
.opt{text-align:left;background:var(--panel);color:var(--cream);border:2px solid var(--rail);border-radius:5px;padding:13px 15px;font-family:inherit;font-size:14px;line-height:1.45;cursor:pointer;transition:all .15s}
|
|
43
|
+
.opt:hover{border-color:var(--cyan);background:var(--rail)}
|
|
44
|
+
.opt.sel{border-color:var(--yellow);background:rgba(255,212,59,.08)}
|
|
45
|
+
.opt.correct{border-color:var(--green);background:rgba(105,219,124,.15);color:var(--green)}
|
|
46
|
+
.opt.wrong{border-color:var(--red);background:rgba(255,107,107,.15);color:var(--red)}
|
|
47
|
+
.opt:disabled{cursor:default}
|
|
48
|
+
.mult-hint{font-size:11px;color:var(--muted);margin-bottom:9px}
|
|
49
|
+
.actions{margin-top:16px;display:flex;gap:10px}
|
|
50
|
+
.btn{background:var(--yellow);color:var(--line);font-weight:800;border:2px solid var(--line);border-radius:4px;padding:10px 18px;font-family:inherit;font-size:13px;cursor:pointer}
|
|
51
|
+
.btn:hover{background:var(--orange)}
|
|
52
|
+
.btn:disabled{background:var(--rail);color:var(--muted);cursor:default;opacity:.5}
|
|
53
|
+
.btn.subtle{background:var(--panel);color:var(--cream);border-color:var(--rail)}
|
|
54
|
+
.btn.subtle:hover{background:var(--rail);color:var(--cream)}
|
|
55
|
+
/* 反馈 */
|
|
56
|
+
.feedback{margin-top:18px;display:none;animation:fade .2s}
|
|
57
|
+
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
58
|
+
.feedback.show{display:block}
|
|
59
|
+
.verdict{font-size:16px;font-weight:800;margin-bottom:8px;display:flex;align-items:center;gap:8px}
|
|
60
|
+
.verdict.ok{color:var(--green)} .verdict.no{color:var(--red)} .verdict.pending{color:var(--orange)}
|
|
61
|
+
.explain{font-size:13px;line-height:1.7;color:var(--cream);background:var(--deck);border-left:3px solid var(--yellow);padding:12px 14px;border-radius:3px;white-space:pre-wrap}
|
|
62
|
+
.explain .lab{color:var(--cyan);font-weight:700}
|
|
63
|
+
.feynman-zone{margin-top:14px}
|
|
64
|
+
.feynman-zone textarea{width:100%;min-height:120px;background:var(--panel);color:var(--cream);border:2px solid var(--rail);border-radius:5px;padding:12px;font-family:inherit;font-size:13px;line-height:1.55;resize:vertical}
|
|
65
|
+
.feynman-zone textarea:focus{outline:none;border-color:var(--yellow)}
|
|
66
|
+
.fpoints{margin-top:10px;font-size:12px;color:var(--muted)}
|
|
67
|
+
.fpoints .lab{color:var(--cyan)}
|
|
68
|
+
/* 底部进度 */
|
|
69
|
+
.quiz-foot{position:sticky;bottom:0;background:var(--deck);border-top:3px solid var(--line);padding:9px 14px;display:flex;justify-content:space-between;align-items:center;gap:10px;max-width:760px;margin:0 auto}
|
|
70
|
+
.quiz-foot .last-error{font-size:11px;color:var(--red);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;opacity:.85}
|
|
71
|
+
/* ===== 结构图视图 ===== */
|
|
72
|
+
.graph-stage{position:relative;height:100%;background:radial-gradient(ellipse at center,rgba(53,216,255,.05) 0%,transparent 60%),var(--base)}
|
|
73
|
+
.graph-stage svg{width:100%;height:100%;display:block}
|
|
74
|
+
.gbar{position:absolute;top:8px;left:8px;display:flex;gap:8px;align-items:center;background:rgba(7,17,27,.7);padding:6px 10px;border-radius:4px;border:1px solid var(--rail);font-size:11px}
|
|
75
|
+
.legend{position:absolute;bottom:8px;left:8px;background:rgba(7,17,27,.7);border:1px solid var(--rail);border-radius:4px;padding:8px 10px;font-size:11px;color:var(--muted)}
|
|
76
|
+
.legend .li{display:flex;align-items:center;gap:6px;margin:2px 0}
|
|
77
|
+
.legend .sw{width:12px;height:12px;border-radius:50%;border:1px solid var(--line)}
|
|
78
|
+
.node-detail{position:absolute;top:8px;right:8px;width:280px;max-height:calc(100% - 16px);overflow-y:auto;background:var(--deck);border:2px solid var(--yellow);border-radius:5px;padding:14px;display:none}
|
|
79
|
+
.node-detail.show{display:block;animation:fade .15s}
|
|
80
|
+
.node-detail h4{color:var(--yellow);font-size:15px;margin-bottom:8px}
|
|
81
|
+
.node-detail .mastery-row{font-size:12px;color:var(--cyan);margin-bottom:10px}
|
|
82
|
+
.node-detail p{font-size:12px;line-height:1.6;color:var(--cream);margin-bottom:8px}
|
|
83
|
+
.node-detail .ex{font-size:11px;color:var(--cyan);border-left:2px solid var(--cyan);padding-left:8px;line-height:1.5}
|
|
84
|
+
.gnode{cursor:pointer}
|
|
85
|
+
.gnode:hover .gn-rect{stroke:var(--yellow);stroke-width:2.5}
|
|
86
|
+
.gnode text{font-size:12px;font-weight:600;fill:var(--cream);pointer-events:none}
|
|
87
|
+
.pre-edge{fill:none;stroke:var(--rail);stroke-width:1.8;opacity:0.75}
|
|
88
|
+
.sem-edge{fill:none;stroke:var(--cyan);stroke-width:1.3;stroke-dasharray:5 4;opacity:0.5}
|
|
89
|
+
.sem-label{font-size:9px;fill:var(--cyan);text-anchor:middle;pointer-events:none}
|
|
90
|
+
.sem-label-bg{fill:var(--line);opacity:0.85}
|
|
91
|
+
/* ===== 结算页 ===== */
|
|
92
|
+
.settle{max-width:760px;margin:0 auto;padding:30px 22px 80px}
|
|
93
|
+
.settle h2{color:var(--yellow);font-size:22px;font-weight:800;margin-bottom:6px}
|
|
94
|
+
.settle .sub{color:var(--cyan);font-size:13px;margin-bottom:22px}
|
|
95
|
+
.settle .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:14px;margin-bottom:26px}
|
|
96
|
+
.settle .sc{background:var(--panel);border:1px solid var(--rail);border-radius:5px;padding:14px;text-align:center}
|
|
97
|
+
.settle .sc .n{font-size:26px;font-weight:800;color:var(--yellow)}
|
|
98
|
+
.settle .sc.g .n{color:var(--green)} .settle .sc.b .n{color:var(--red)}
|
|
99
|
+
.settle .sc .l{font-size:11px;color:var(--muted);margin-top:2px}
|
|
100
|
+
.settle h3{color:var(--cyan);font-size:13px;text-transform:uppercase;letter-spacing:1px;margin:24px 0 12px}
|
|
101
|
+
.errlist{display:flex;flex-direction:column;gap:10px}
|
|
102
|
+
.errcard{background:var(--deck);border-left:3px solid var(--red);border-radius:3px;padding:12px 14px}
|
|
103
|
+
.errcard .et{font-size:13px;color:var(--cream);margin-bottom:6px}
|
|
104
|
+
.errcard .ek{font-size:12px;color:var(--muted);line-height:1.6;white-space:pre-wrap}
|
|
105
|
+
.errcard .ek .lab{color:var(--cyan)}
|
|
106
|
+
.errcard .ek .ans{color:var(--green)}
|
|
107
|
+
.next-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
108
|
+
.next-btn{text-align:left;background:var(--panel);color:var(--cream);border:2px solid var(--rail);border-radius:5px;padding:13px 15px;font-family:inherit;font-size:13px;line-height:1.4;cursor:pointer}
|
|
109
|
+
.next-btn:hover{border-color:var(--yellow);background:var(--rail)}
|
|
110
|
+
.next-btn b{display:block;color:var(--yellow);margin-bottom:3px}
|
|
111
|
+
.next-btn small{color:var(--muted);font-size:11px}
|
|
112
|
+
</style>
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<!-- 顶栏 -->
|
|
116
|
+
<div class="topbar">
|
|
117
|
+
<span class="domain" id="domain">域</span>
|
|
118
|
+
<span class="title" id="title">Study</span>
|
|
119
|
+
<div class="seg">
|
|
120
|
+
<button id="tab-quiz" class="active" onclick="showView('quiz')">📝 答题</button>
|
|
121
|
+
<button id="tab-graph" onclick="showView('graph')">📊 结构图</button>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="progress-wrap">
|
|
124
|
+
<span class="stat-pill good" id="learned-pill">已会 0/0</span>
|
|
125
|
+
<div class="pbar"><div class="pfill" id="pfill" style="width:0%"></div></div>
|
|
126
|
+
<span class="stat-pill" id="qpos">0/0</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- 答题视图 -->
|
|
131
|
+
<div class="view active" id="view-quiz">
|
|
132
|
+
<div class="quiz-stage" id="quiz-stage"></div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- 结构图视图 -->
|
|
136
|
+
<div class="view" id="view-graph">
|
|
137
|
+
<div class="graph-stage" id="graph-stage">
|
|
138
|
+
<svg id="graph-svg" viewBox="0 0 900 640" preserveAspectRatio="xMidYMid meet"></svg>
|
|
139
|
+
<div class="gbar"><span id="gbar-txt" style="color:var(--cream)">知识导图 · 节点=知识点(左条=掌握度) · 点击看详情</span></div>
|
|
140
|
+
<div class="legend">
|
|
141
|
+
<div class="li"><span class="sw" style="background:#2e5266"></span>未练</div>
|
|
142
|
+
<div class="li"><span class="sw" style="background:#ffb454"></span>学习中</div>
|
|
143
|
+
<div class="li"><span class="sw" style="background:#69db7c"></span>已会</div>
|
|
144
|
+
<div class="li" style="margin-top:4px;border-top:1px solid var(--rail);padding-top:5px"><span class="sw" style="background:var(--rail);height:2px"></span>实线=学习依赖</div>
|
|
145
|
+
<div class="li"><span class="sw" style="background:var(--cyan);height:2px;border-top:1px dashed var(--cyan)"></span>虚线=语义关系</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="node-detail" id="node-detail"></div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<script>
|
|
152
|
+
const DATA = __GS_DATA__;
|
|
153
|
+
const INPUT_ID = "__GS_INPUT_ID__";
|
|
154
|
+
const PANEL_ID = "__GS_PANEL_ID__";
|
|
155
|
+
const INTERACTIVE = __GS_INTERACTIVE__;
|
|
156
|
+
const QUEST = DATA.quest || {};
|
|
157
|
+
const PROGRESS = DATA.progress || {};
|
|
158
|
+
const DUE = new Set(DATA.due || []);
|
|
159
|
+
const cells = (QUEST.cells||[]).filter(c=>c.id);
|
|
160
|
+
const cellMap = {}; cells.forEach(c=>cellMap[c.id]=c);
|
|
161
|
+
const relations = QUEST.relations||[];
|
|
162
|
+
const masters = (PROGRESS.cells||{});
|
|
163
|
+
let masteryOf = id => { const m=masters[id]; return m&&typeof m.mastery==='number'?m.mastery:0; };
|
|
164
|
+
let learnedOf = id => masteryOf(id)>=0.7 ? 1 : 0;
|
|
165
|
+
|
|
166
|
+
// ===== 工具 =====
|
|
167
|
+
const $=id=>document.getElementById(id);
|
|
168
|
+
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
|
169
|
+
function cellMasteryColor(id){
|
|
170
|
+
const m=masteryOf(id);
|
|
171
|
+
if(m<=0) return '#2e5266';
|
|
172
|
+
if(m<0.7) return '#ffb454';
|
|
173
|
+
return '#69db7c';
|
|
174
|
+
}
|
|
175
|
+
function cellRefs(q){const c=q.cell; if(typeof c==='string')return [c]; if(Array.isArray(c))return c; return [];}
|
|
176
|
+
|
|
177
|
+
// ===== 本轮选题 =====
|
|
178
|
+
const session=QUEST.session||{};
|
|
179
|
+
const LEN=Math.min(Math.max(session.length||8,3),30);
|
|
180
|
+
const SHUFFLE=session.shuffle!==false;
|
|
181
|
+
const REVIEW=session.review_mode===true;
|
|
182
|
+
function pickQuestions(){
|
|
183
|
+
const qs=QUEST.questions||[];
|
|
184
|
+
let pool=qs.slice();
|
|
185
|
+
if(REVIEW && DUE.size>0){
|
|
186
|
+
const due=qs.filter(q=>DUE.has(q.id));
|
|
187
|
+
const rest=qs.filter(q=>!DUE.has(q.id));
|
|
188
|
+
pool=due.concat(rest);
|
|
189
|
+
}
|
|
190
|
+
// 首次学习:按知识点 prereq 拓扑优先 + 低掌握度优先
|
|
191
|
+
if(!REVIEW){
|
|
192
|
+
pool.sort((a,b)=>{
|
|
193
|
+
const ma=Math.max(0,...cellRefs(a).map(masteryOf));
|
|
194
|
+
const mb=Math.max(0,...cellRefs(b).map(masteryOf));
|
|
195
|
+
if(Math.abs(ma-mb)>0.001) return ma-mb; // 低掌握度先
|
|
196
|
+
const da=(QUEST.cells.find(c=>c.id===cellRefs(a)[0])||{}).difficulty||3;
|
|
197
|
+
const db=(QUEST.cells.find(c=>c.id===cellRefs(b)[0])||{}).difficulty||3;
|
|
198
|
+
return da-db;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
let chosen=pool.slice(0,LEN);
|
|
202
|
+
if(SHUFFLE){for(let i=chosen.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[chosen[i],chosen[j]]=[chosen[j],chosen[i]];}}
|
|
203
|
+
return chosen;
|
|
204
|
+
}
|
|
205
|
+
let QUESTIONS=pickQuestions();
|
|
206
|
+
let qi=0; // 当前题序
|
|
207
|
+
let answers=[]; // 本轮答题记录 [{qid,verdict,cells,chosen,answer,explain}]
|
|
208
|
+
let lastError=null; // 上题错因(底栏)
|
|
209
|
+
|
|
210
|
+
// ===== 答题渲染 =====
|
|
211
|
+
function renderQuiz(){
|
|
212
|
+
if(qi>=QUESTIONS.length){ renderSettle(); return; }
|
|
213
|
+
const q=QUESTIONS[qi];
|
|
214
|
+
const stage=$('quiz-stage');
|
|
215
|
+
const refs=cellRefs(q);
|
|
216
|
+
const topicHtml=refs.map(r=>`<span class="chip">${esc(cellMap[r]?cellMap[r].label:r)}</span>`).join('');
|
|
217
|
+
let typeHint = q.type==='multi'?'<div class="mult-hint">多选题:可选多项,全对才得分</div>':''
|
|
218
|
+
+(q.type==='connect'?'<div class="mult-hint">关系判断:选出正确的关系标签</div>':'')
|
|
219
|
+
+(q.type==='feynman'?'<div class="mult-hint">费曼题:用自己的话解释,由 CLI 侧 agent 判定</div>':'');
|
|
220
|
+
let optsHtml='';
|
|
221
|
+
if(q.type==='choice'||q.type==='connect'){
|
|
222
|
+
optsHtml='<div class="options">'+(q.options||[]).map((o,i)=>`<button class="opt" data-i="${i}" onclick="pick(${i})">${esc(o.label)}</button>`).join('')+'</div>';
|
|
223
|
+
}else if(q.type==='multi'){
|
|
224
|
+
optsHtml='<div class="options">'+(q.options||[]).map((o,i)=>`<button class="opt" data-i="${i}" onclick="toggle(${i})">${esc(o.label)}</button>`).join('')+'</div>';
|
|
225
|
+
}
|
|
226
|
+
stage.innerHTML=`
|
|
227
|
+
<div class="qhead"><span><span class="idx">第 ${qi+1} 题</span> / 共 ${QUESTIONS.length} 题</span><span>${q.type}</span></div>
|
|
228
|
+
<div class="qtopic">关联知识点 ${topicHtml}</div>
|
|
229
|
+
<div class="prompt">${esc(q.prompt)}</div>
|
|
230
|
+
${typeHint}
|
|
231
|
+
${optsHtml}
|
|
232
|
+
<div class="actions">
|
|
233
|
+
${q.type==='multi'?`<button class="btn" id="submit-multi" disabled onclick="submitMulti()">提交答案</button>`:''}
|
|
234
|
+
${q.type==='feynman'?`<button class="btn" onclick="renderFeynman()">开始解释</button>`:''}
|
|
235
|
+
</div>
|
|
236
|
+
<div class="feedback" id="feedback"></div>`;
|
|
237
|
+
updateHeader();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let picked=-1, multiSet=new Set();
|
|
241
|
+
function pick(i){
|
|
242
|
+
if(document.querySelector('.opt.correct,.opt.wrong')) return; // 已答
|
|
243
|
+
const opts=document.querySelectorAll('.opt');
|
|
244
|
+
opts.forEach(o=>o.classList.remove('sel'));
|
|
245
|
+
opts[i].classList.add('sel');
|
|
246
|
+
picked=i;
|
|
247
|
+
const q=QUESTIONS[qi];
|
|
248
|
+
if(q.type==='choice'){ confirmChoice(q,i); }
|
|
249
|
+
else if(q.type==='connect'){ confirmConnect(q,i); }
|
|
250
|
+
}
|
|
251
|
+
function toggle(i){
|
|
252
|
+
if(document.querySelector('.opt.correct,.opt.wrong')) return;
|
|
253
|
+
const btn=document.querySelector(`.opt[data-i="${i}"]`);
|
|
254
|
+
if(multiSet.has(i)){multiSet.delete(i);btn.classList.remove('sel');}
|
|
255
|
+
else{multiSet.add(i);btn.classList.add('sel');}
|
|
256
|
+
$('submit-multi').disabled=multiSet.size===0;
|
|
257
|
+
}
|
|
258
|
+
function submitMulti(){ const q=QUESTIONS[qi]; confirmMulti(q,[...multiSet]); }
|
|
259
|
+
|
|
260
|
+
function confirmChoice(q,i){
|
|
261
|
+
const opts=q.options||[];
|
|
262
|
+
const correct=opts[i]&&opts[i].correct;
|
|
263
|
+
const chosen=opts[i]?opts[i].label:'';
|
|
264
|
+
const correctOpt=opts.find(o=>o.correct);
|
|
265
|
+
showFeedback(q, correct?'correct':'wrong', correctOpt?correctOpt.label:'', chosen);
|
|
266
|
+
recordAnswer(q, correct?'correct':'wrong', chosen);
|
|
267
|
+
}
|
|
268
|
+
function confirmConnect(q,i){
|
|
269
|
+
const opts=q.options||[];
|
|
270
|
+
const correct=opts[i]&&(opts[i].is===q.answer || opts[i].label===q.answer);
|
|
271
|
+
const chosen=opts[i]?opts[i].label:'';
|
|
272
|
+
showFeedback(q, correct?'correct':'wrong', q.answer, chosen);
|
|
273
|
+
recordAnswer(q, correct?'correct':'wrong', chosen);
|
|
274
|
+
}
|
|
275
|
+
function confirmMulti(q,idxs){
|
|
276
|
+
const opts=q.options||[];
|
|
277
|
+
const allCorrect=opts.every((o,i)=>(o.correct)===idxs.includes(i));
|
|
278
|
+
const chosen=idxs.map(i=>opts[i]?opts[i].label:'').join(' / ');
|
|
279
|
+
const ans=opts.filter(o=>o.correct).map(o=>o.label).join(' / ');
|
|
280
|
+
showFeedback(q, allCorrect?'correct':'wrong', ans, chosen);
|
|
281
|
+
recordAnswer(q, allCorrect?'correct':'wrong', chosen);
|
|
282
|
+
}
|
|
283
|
+
function renderFeynman(){
|
|
284
|
+
const q=QUESTIONS[qi];
|
|
285
|
+
const stage=$('quiz-stage');
|
|
286
|
+
const fb=$('feedback');
|
|
287
|
+
fb.classList.add('show');
|
|
288
|
+
const pts=q.feynman_points&&q.feynman_points.length?`<div class="fpoints"><span class="lab">要点:</span>${q.feynman_points.map(p=>`<div>· ${esc(p)}</div>`).join('')}</div>`:'';
|
|
289
|
+
fb.innerHTML=`<div class="verdict pending" id="vmark">✎ 费曼解释</div>
|
|
290
|
+
<div class="feynman-zone">
|
|
291
|
+
<textarea id="feynman-text" placeholder="用自己的话把这个概念讲清楚,仿佛在教一个初学者…"></textarea>
|
|
292
|
+
${pts}
|
|
293
|
+
<div class="actions"><button class="btn" onclick="submitFeynman()">提交解释 → 回传 CLI 判定</button></div>
|
|
294
|
+
</div>`;
|
|
295
|
+
}
|
|
296
|
+
function submitFeynman(){
|
|
297
|
+
const q=QUESTIONS[qi];
|
|
298
|
+
const txt=$('feynman-text').value.trim();
|
|
299
|
+
if(!txt){return;}
|
|
300
|
+
showFeedback(q,'pending',q.answer||'(参考解释见 CLI 判定)',txt);
|
|
301
|
+
recordAnswer(q,'pending',txt,feynmanAnswerTxt);
|
|
302
|
+
}
|
|
303
|
+
function showFeedback(q,verdict,answer,chosen){
|
|
304
|
+
const fb=$('feedback'); fb.classList.add('show');
|
|
305
|
+
const refs=cellRefs(q);
|
|
306
|
+
let html=`<div class="verdict ${verdict==='correct'?'ok':verdict==='wrong'?'no':'pending'}" id="vmark">${verdict==='correct'?'✓ 答对':verdict==='wrong'?'✗ 答错':'✎ 已提交,待 CLI 判定'}</div>`;
|
|
307
|
+
let body='';
|
|
308
|
+
if(verdict==='wrong'){
|
|
309
|
+
body+=`<div><span class="lab">你选的:</span>${esc(chosen)}</div>`;
|
|
310
|
+
body+=`<div><span class="lab">正解:</span><span class="ans">${esc(answer)}</span></div>`;
|
|
311
|
+
if(q.explain) body+=`<div style="margin-top:8px"><span class="lab">解析:</span>${esc(q.explain)}</div>`;
|
|
312
|
+
lastError=(q.explain||`${refs.map(r=>cellMap[r]?cellMap[r].label:r).join('·')} ← ${esc(answer)}`).slice(0,80);
|
|
313
|
+
}else if(verdict==='correct'){
|
|
314
|
+
if(q.explain) body+=`<div><span class="lab">解析:</span>${esc(q.explain)}</div>`;
|
|
315
|
+
}else if(verdict==='pending'){
|
|
316
|
+
body+=`<div><span class="lab">参考:</span>${esc(answer)}</div>`;
|
|
317
|
+
if(q.feynman_points&&q.feynman_points.length) body+=`<div style="margin-top:6px"><span class="lab">要点应覆盖:</span>${q.feynman_points.map(p=>esc(p)).join(';')}</div>`;
|
|
318
|
+
lastError='费曼题待 CLI 判定';
|
|
319
|
+
}
|
|
320
|
+
html+=`<div class="explain">${body||'<span class="lab">继续</span>'}</div>`;
|
|
321
|
+
const isLast=qi>=QUESTIONS.length-1;
|
|
322
|
+
html+=`<div class="actions"><button class="btn" onclick="nextQ()">${isLast?'完成本轮 → 查看复盘':'下一题 →'}</button></div>`;
|
|
323
|
+
fb.innerHTML=html;
|
|
324
|
+
// 锁定选项不可再点
|
|
325
|
+
document.querySelectorAll('.opt').forEach(o=>o.disabled=true);
|
|
326
|
+
updateHeader();
|
|
327
|
+
}
|
|
328
|
+
function recordAnswer(q,verdict,chosen,answerField){
|
|
329
|
+
const refs=cellRefs(q);
|
|
330
|
+
answers.push({qid:q.id,verdict,cells:refs,chosen,answer:q.answer||answerField||'',explain:q.explain||''});
|
|
331
|
+
}
|
|
332
|
+
function nextQ(){
|
|
333
|
+
qi++; picked=-1; multiSet.clear();
|
|
334
|
+
renderQuiz();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ===== 头部进度 =====
|
|
338
|
+
function updateHeader(){
|
|
339
|
+
$('qpos').textContent=`${Math.min(qi+ (answers.length?1:0),QUESTIONS.length)}/${QUESTIONS.length}`;
|
|
340
|
+
const total=cells.length||1;
|
|
341
|
+
const learned=cells.filter(c=>learnedOf(c.id)).length;
|
|
342
|
+
$('learned-pill').textContent=`已会 ${learned}/${total}`;
|
|
343
|
+
$('learned-pill').className='stat-pill '+(learned>=total?'good':'');
|
|
344
|
+
const pct=Math.round((qi/QUESTIONS.length)*100);
|
|
345
|
+
$('pfill').style.width=pct+'%';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ===== 结算页 =====
|
|
349
|
+
function renderSettle(){
|
|
350
|
+
const stage=$('quiz-stage');
|
|
351
|
+
const correct=answers.filter(a=>a.verdict==='correct').length;
|
|
352
|
+
const wrong=answers.filter(a=>a.verdict==='wrong').length;
|
|
353
|
+
const pend=answers.filter(a=>a.verdict==='pending').length;
|
|
354
|
+
const total=QUESTIONS.length;
|
|
355
|
+
const errs=answers.filter(a=>a.verdict==='wrong');
|
|
356
|
+
let stats=`<div class="sc"><div class="n">${total}</div><div class="l">本轮题数</div></div>
|
|
357
|
+
<div class="sc g"><div class="n">${correct}</div><div class="l">答对</div></div>
|
|
358
|
+
<div class="sc b"><div class="n">${wrong}</div><div class="l">答错</div></div>
|
|
359
|
+
<div class="sc"><div class="n" style="color:var(--orange)">${pend}</div><div class="l">费曼待判</div></div>`;
|
|
360
|
+
// 掌握度变化
|
|
361
|
+
const learned=cells.filter(c=>learnedOf(c.id)).length;
|
|
362
|
+
stats+=`<div class="sc g"><div class="n">${learned}/${cells.length}</div><div class="l">已会知识点</div></div>`;
|
|
363
|
+
// 错题复盘
|
|
364
|
+
let errHtml = errs.length? errs.map(e=>{
|
|
365
|
+
const q=QUESTIONS.find(x=>x.id===e.qid)||{};
|
|
366
|
+
const refs=cellRefs(q);
|
|
367
|
+
return `<div class="errcard"><div class="et">${esc(q.prompt||e.qid)}</div>
|
|
368
|
+
<div class="ek"><span class="lab">关联:</span>${refs.map(r=>esc(cellMap[r]?cellMap[r].label:r)).join(' · ')}</div>
|
|
369
|
+
<div class="ek"><span class="lab">你选:</span>${esc(e.chosen)} <span class="lab">正解:</span><span class="ans">${esc(e.answer)}</span></div>
|
|
370
|
+
${q.explain?`<div class="ek"><span class="lab">解析:</span>${esc(q.explain)}</div>`:''}</div>`;
|
|
371
|
+
}).join('') : '<div class="errcard" style="border-left-color:var(--green)"><div class="ek">本轮无错题 ✓</div></div>';
|
|
372
|
+
// 费曼待判
|
|
373
|
+
let feynmanHtml='';
|
|
374
|
+
if(pend>0){
|
|
375
|
+
const pends=answers.filter(a=>a.verdict==='pending');
|
|
376
|
+
feynmanHtml=`<h3>费曼解释(待 CLI agent 判定)</h3>`+pends.map(e=>{
|
|
377
|
+
const q=QUESTIONS.find(x=>x.id===e.qid)||{};
|
|
378
|
+
return `<div class="errcard" style="border-left-color:var(--orange)"><div class="et">${esc(q.prompt||e.qid)}</div>
|
|
379
|
+
<div class="ek"><span class="lab">你的解释:</span>${esc(e.chosen)}</div>
|
|
380
|
+
${q.feynman_points&&q.feynman_points.length?`<div class="ek"><span class="lab">要点:</span>${q.feynman_points.map(p=>esc(p)).join(';')}</div>`:''}</div>`;
|
|
381
|
+
}).join('');
|
|
382
|
+
}
|
|
383
|
+
// 下一步
|
|
384
|
+
const nextOpts=QUEST.next||[];
|
|
385
|
+
let nextHtml = nextOpts.length? `<div class="next-grid">`+nextOpts.map(n=>`<button class="next-btn" data-nid="${esc(n.id)}" onclick="finish('${esc(n.id)}')"><b>${esc(n.label)}</b><small>${esc(n.description||'')}</small></button>`).join('')+`</div>` : '';
|
|
386
|
+
stage.innerHTML=`<div class="settle">
|
|
387
|
+
<h2>本轮学习复盘</h2>
|
|
388
|
+
<div class="sub">对 ${total} 题,答对 ${correct},答错 ${wrong}${pend?`, ${pend} 题费曼待 CLI 判定`:''}</div>
|
|
389
|
+
<div class="stats">${stats}</div>
|
|
390
|
+
<h3>错题复盘</h3>
|
|
391
|
+
<div class="errlist">${errHtml}</div>
|
|
392
|
+
${feynmanHtml}
|
|
393
|
+
${nextHtml?`<h3>下一步</h3>${nextHtml}`:''}
|
|
394
|
+
<h3>操作</h3>
|
|
395
|
+
<div class="actions">
|
|
396
|
+
<button class="btn" onclick="finish()">发送本轮记录到 CLI</button>
|
|
397
|
+
<button class="btn subtle" onclick="restart()">同域再来一趟</button>
|
|
398
|
+
<button class="btn subtle" onclick="showView('graph')">查看完整结构图</button>
|
|
399
|
+
</div></div>`;
|
|
400
|
+
updateHeader();
|
|
401
|
+
}
|
|
402
|
+
function restart(){
|
|
403
|
+
QUESTIONS=pickQuestions(); qi=0; answers=[]; lastError=null;
|
|
404
|
+
renderQuiz();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ===== 完成回传 =====
|
|
408
|
+
let sent=false;
|
|
409
|
+
function finish(nextId){
|
|
410
|
+
if(!INTERACTIVE||sent) return;
|
|
411
|
+
sent=true;
|
|
412
|
+
const lines=[];
|
|
413
|
+
lines.push('## game-study 本轮学习记录','');
|
|
414
|
+
lines.push(`- 主题: ${QUEST.domain||QUEST.title}`);
|
|
415
|
+
lines.push(`- 本轮题数: ${QUESTIONS.length}`);
|
|
416
|
+
lines.push(`- 答对: ${answers.filter(a=>a.verdict==='correct').length}`);
|
|
417
|
+
lines.push(`- 答错: ${answers.filter(a=>a.verdict==='wrong').length}`);
|
|
418
|
+
lines.push(`- 费曼待判: ${answers.filter(a=>a.verdict==='pending').length}`);
|
|
419
|
+
lines.push(`- 已会知识点: ${cells.filter(c=>learnedOf(c.id)).length}/${cells.length}`);
|
|
420
|
+
lines.push('','### 答题明细','');
|
|
421
|
+
answers.forEach(a=>{
|
|
422
|
+
const q=QUESTIONS.find(x=>x.id===a.qid)||{};
|
|
423
|
+
const refs=cellRefs(q).map(r=>cellMap[r]?cellMap[r].label:r).join(',');
|
|
424
|
+
lines.push(`- [${a.qid}] ${a.verdict} | ${refs} | 你选: ${a.chosen} | 正解: ${a.answer}`);
|
|
425
|
+
});
|
|
426
|
+
if(nextId){const n=(QUEST.next||[]).find(o=>o.id===nextId); if(n) lines.push('','### 下一步选择','',`- ${n.label}: ${n.description||''}`);}
|
|
427
|
+
// 错题要点
|
|
428
|
+
const errs=answers.filter(a=>a.verdict==='wrong');
|
|
429
|
+
if(errs.length){lines.push('','### 错题要点',''); errs.forEach(e=>{const q=QUESTIONS.find(x=>x.id===e.qid)||{}; lines.push(`- ${q.prompt}: 选 ${e.chosen}(正解 ${e.answer})${q.explain?` — ${q.explain}`:''}`);});}
|
|
430
|
+
// 费曼待判
|
|
431
|
+
const pends=answers.filter(a=>a.verdict==='pending');
|
|
432
|
+
if(pends.length){lines.push('','### 费曼解释(请你判定 pass/fail)',''); pends.forEach(e=>{const q=QUESTIONS.find(x=>x.id===e.qid)||{}; lines.push(`- [${e.qid}] ${q.prompt}`); lines.push(` 解释: ${e.chosen}`); if(q.feynman_points&&q.feynman_points.length) lines.push(` 要点: ${q.feynman_points.join(';')}`); lines.push(` 判定后请运行: localweb.py study-record --quest-id ${DATA.quest_id||QUEST.id||''} --qid ${e.qid} --verdict <pass|fail> --feedback "..."`);});}
|
|
433
|
+
window.parent.postMessage({
|
|
434
|
+
localweb:true, type:'panel_input', input_id:INPUT_ID, text:lines.join('\n'),
|
|
435
|
+
label:'game-study run record', panel_id:PANEL_ID,
|
|
436
|
+
meta:{mode:'game-study', quest_id:DATA.quest_id||QUEST.id||'', results:answers.map(a=>({qid:a.qid,verdict:a.verdict,cells:a.cells,chosen:a.chosen,answer:a.answer,explain:a.explain}))}
|
|
437
|
+
},'*');
|
|
438
|
+
const b=event.target.closest('.btn'); if(b){b.textContent='已发送 ✓';b.disabled=true;}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ===== 结构图视图 =====
|
|
442
|
+
function showView(v){
|
|
443
|
+
document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));
|
|
444
|
+
$('view-'+v).classList.add('active');
|
|
445
|
+
$('tab-quiz').classList.toggle('active',v==='quiz');
|
|
446
|
+
$('tab-graph').classList.toggle('active',v==='graph');
|
|
447
|
+
if(v==='graph') renderGraph();
|
|
448
|
+
}
|
|
449
|
+
function renderGraph(){
|
|
450
|
+
const svg=$('graph-svg'); svg.innerHTML='';
|
|
451
|
+
const H=640, COL0=60, COLW=200, YMAR=46, NW=150, NH=34;
|
|
452
|
+
const layerOf={};
|
|
453
|
+
const lyr=id=>{if(id in layerOf)return layerOf[id];const c=cellMap[id];if(!c)return 0;const ps=(c.prereq||[]).filter(p=>cellMap[p]);if(!ps.length)return layerOf[id]=0;return layerOf[id]=1+Math.max(...ps.map(lyr));};
|
|
454
|
+
cells.forEach(c=>layerOf[c.id]=lyr(c.id));
|
|
455
|
+
const byL={}; cells.forEach(c=>{(byL[layerOf[c.id]]=byL[layerOf[c.id]]||[]).push(c);});
|
|
456
|
+
const pos={};
|
|
457
|
+
Object.keys(byL).map(Number).sort((a,b)=>a-b).forEach(L=>{
|
|
458
|
+
const arr=byL[L];
|
|
459
|
+
arr.sort((a,b)=>{
|
|
460
|
+
const ya=Math.min(320,...(a.prereq||[]).filter(p=>pos[p]).map(p=>pos[p].y));
|
|
461
|
+
const yb=Math.min(320,...(b.prereq||[]).filter(p=>pos[p]).map(p=>pos[p].y));
|
|
462
|
+
return ya-yb;
|
|
463
|
+
});
|
|
464
|
+
const usable=H-2*YMAR;
|
|
465
|
+
arr.forEach((c,i)=>pos[c.id]={x:COL0+L*COLW, y:YMAR+usable*(i+0.5)/arr.length});
|
|
466
|
+
});
|
|
467
|
+
const prePairs=new Set();
|
|
468
|
+
cells.forEach(c=>(c.prereq||[]).forEach(p=>{if(cellMap[p]&&pos[p]&&pos[c.id])prePairs.add(p+'>'+c.id);}));
|
|
469
|
+
const NS='http://www.w3.org/2000/svg';
|
|
470
|
+
const mk=(t,a)=>{const e=document.createElementNS(NS,t);for(const k in a)e.setAttribute(k,a[k]);return e;};
|
|
471
|
+
function semEdge(e){
|
|
472
|
+
if(!pos[e.from]||!pos[e.to])return;
|
|
473
|
+
if(prePairs.has(e.from+'>'+e.to))return;
|
|
474
|
+
const a=pos[e.from],b=pos[e.to];
|
|
475
|
+
const sgn=Math.sign(b.x-a.x)||1;
|
|
476
|
+
const s={x:sgn>0?a.x+NW/2:a.x-NW/2,y:a.y};
|
|
477
|
+
const t={x:sgn>0?b.x-NW/2:b.x+NW/2,y:b.y};
|
|
478
|
+
const mx=(s.x+t.x)/2, my=(s.y+t.y)/2-45;
|
|
479
|
+
svg.appendChild(mk('path',{d:`M ${s.x} ${s.y} Q ${mx} ${my} ${t.x} ${t.y}`,class:'sem-edge'}));
|
|
480
|
+
if(e.label){
|
|
481
|
+
svg.appendChild(mk('rect',{x:mx-26,y:my-9,width:52,height:13,rx:2,class:'sem-label-bg'}));
|
|
482
|
+
const tx=mk('text',{x:mx,y:my,class:'sem-label'}); tx.textContent=e.label; svg.appendChild(tx);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function preEdge(p,cid){
|
|
486
|
+
const a=pos[p],b=pos[cid]; if(!a||!b)return;
|
|
487
|
+
const ax=a.x+NW/2,bx=b.x-NW/2,mx=ax+(bx-ax)*0.5;
|
|
488
|
+
svg.appendChild(mk('path',{d:`M ${ax} ${a.y} L ${mx} ${a.y} L ${mx} ${b.y} L ${bx} ${b.y}`,class:'pre-edge'}));
|
|
489
|
+
}
|
|
490
|
+
relations.forEach(semEdge);
|
|
491
|
+
cells.forEach(c=>(c.prereq||[]).forEach(p=>preEdge(p,c.id)));
|
|
492
|
+
cells.forEach(c=>{
|
|
493
|
+
const p=pos[c.id]; if(!p)return; const L=layerOf[c.id];
|
|
494
|
+
const g=mk('g',{class:'gnode'}); g.dataset.cid=c.id;
|
|
495
|
+
const m=masteryOf(c.id);
|
|
496
|
+
const stroke=m>=0.7?'#69db7c':(m>0?'#ffb454':'#3a5d78');
|
|
497
|
+
g.appendChild(mk('rect',{x:p.x-NW/2,y:p.y-NH/2,width:NW,height:NH,rx:5,fill:L===0?'#2d5d7f':'#1b3a4f',stroke:stroke,'stroke-width':'2',class:'gn-rect'}));
|
|
498
|
+
g.appendChild(mk('rect',{x:p.x-NW/2,y:p.y-NH/2,width:5,height:NH,fill:cellMasteryColor(c.id)}));
|
|
499
|
+
const tx=mk('text',{x:p.x-NW/2+13,y:p.y+4,'text-anchor':'start',fill:'#fff8d6'}); tx.textContent=c.label; g.appendChild(tx);
|
|
500
|
+
g.addEventListener('click',()=>showNodeDetail(c.id));
|
|
501
|
+
svg.appendChild(g);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
function showNodeDetail(id){
|
|
505
|
+
const c=cellMap[id]; if(!c)return;
|
|
506
|
+
const m=masters[id]||{};
|
|
507
|
+
const bar='<div style="font-size:11px;color:var(--muted);margin-top:8px">掌握度 '+Math.round(masteryOf(id)*100)+'% · 练习 '+(m.reps||0)+'次 · 答对 '+(m.correct||0)+'次</div>';
|
|
508
|
+
$('node-detail').innerHTML='<h4>'+esc(c.label)+'</h4><div class="mastery-row">'+esc(c.tags&&c.tags[0]||'')+'</div><p>'+esc(c.brief||'')+'</p>'+(c.example?'<div class="ex">例: '+esc(c.example)+'</div>':'')+bar;
|
|
509
|
+
$('node-detail').classList.add('show');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ===== 初始化 =====
|
|
513
|
+
renderQuiz();
|
|
514
|
+
</script>
|
|
515
|
+
</body>
|
|
516
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# learn —— 结构化学习面板(面板交互模式·DSH 版)
|
|
2
|
+
|
|
3
|
+
lesson 结构遵循 `schemas/lesson.schema.json`(concept cards / structure / examples / questions);
|
|
4
|
+
示例:`examples/template-showcase.json`(结构示范)、`examples/epsilon-delta.json`(主题示范);
|
|
5
|
+
模板 `templates/lesson.html` 是参考起点,**不套死:内容结构决定布局**。
|
|
6
|
+
|
|
7
|
+
## 投递(DSH 原生)
|
|
8
|
+
|
|
9
|
+
1. 把 lesson 渲染成 self-contained HTML → `stage_panel`;
|
|
10
|
+
2. 含 questions 时逐题展示,需要作答/选择 → `stage_choice`(回答以用户消息到达,由会话判定对错与下一步),无 wait 闸门;
|
|
11
|
+
3. 教学判断、错因审计、下一步选择均由会话负责——面板只是呈现面。
|
|
12
|
+
|
|
13
|
+
## 纪律
|
|
14
|
+
|
|
15
|
+
- 面板内的概念卡/结构图尽量本地交互(hover/tab/折叠),需要用户推进再 `stage_choice`。
|
|
16
|
+
- 纯讲解一轮不需要任何回传,`stage_panel` 后正常结束即可。
|