@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,782 @@
|
|
|
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 Learn</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light;
|
|
10
|
+
--line: #07111b;
|
|
11
|
+
--base: #31566e;
|
|
12
|
+
--deck: #2e4f67;
|
|
13
|
+
--panel: #fff8d6;
|
|
14
|
+
--paper: #ffffff;
|
|
15
|
+
--yellow: #ffd43b;
|
|
16
|
+
--cyan: #35d8ff;
|
|
17
|
+
--orange: #ffb454;
|
|
18
|
+
--green: #9ee493;
|
|
19
|
+
--red: #ff7b7b;
|
|
20
|
+
--muted: #586875;
|
|
21
|
+
--ink: #142331;
|
|
22
|
+
}
|
|
23
|
+
* { box-sizing: border-box; }
|
|
24
|
+
html, body { margin: 0; min-height: 100%; }
|
|
25
|
+
body {
|
|
26
|
+
background:
|
|
27
|
+
linear-gradient(135deg, rgba(53,216,255,.12), transparent 38%),
|
|
28
|
+
repeating-linear-gradient(90deg, rgba(7,17,27,.08) 0 1px, transparent 1px 56px),
|
|
29
|
+
repeating-linear-gradient(0deg, rgba(7,17,27,.07) 0 1px, transparent 1px 56px),
|
|
30
|
+
var(--base);
|
|
31
|
+
color: var(--ink);
|
|
32
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
33
|
+
}
|
|
34
|
+
button, textarea, input { font: inherit; }
|
|
35
|
+
.app {
|
|
36
|
+
min-height: 100vh;
|
|
37
|
+
padding: clamp(14px, 2.5vw, 28px);
|
|
38
|
+
}
|
|
39
|
+
.hero {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
42
|
+
gap: 16px;
|
|
43
|
+
align-items: end;
|
|
44
|
+
border: 4px solid var(--line);
|
|
45
|
+
background: var(--panel);
|
|
46
|
+
box-shadow: 8px 8px 0 var(--line);
|
|
47
|
+
padding: clamp(18px, 3vw, 30px);
|
|
48
|
+
}
|
|
49
|
+
.eyebrow {
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
width: fit-content;
|
|
52
|
+
border: 3px solid var(--line);
|
|
53
|
+
background: var(--yellow);
|
|
54
|
+
padding: 6px 10px;
|
|
55
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
56
|
+
font-size: 13px;
|
|
57
|
+
font-weight: 900;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
}
|
|
60
|
+
h1 {
|
|
61
|
+
margin: 14px 0 8px;
|
|
62
|
+
font-size: clamp(34px, 5.5vw, 72px);
|
|
63
|
+
line-height: .95;
|
|
64
|
+
letter-spacing: 0;
|
|
65
|
+
}
|
|
66
|
+
.subtitle {
|
|
67
|
+
max-width: 78ch;
|
|
68
|
+
margin: 0;
|
|
69
|
+
color: #263b4a;
|
|
70
|
+
font-size: clamp(15px, 1.8vw, 19px);
|
|
71
|
+
line-height: 1.55;
|
|
72
|
+
}
|
|
73
|
+
.stage {
|
|
74
|
+
border: 3px solid var(--line);
|
|
75
|
+
background: var(--deck);
|
|
76
|
+
color: var(--panel);
|
|
77
|
+
min-width: 170px;
|
|
78
|
+
padding: 14px;
|
|
79
|
+
box-shadow: 5px 5px 0 var(--line);
|
|
80
|
+
}
|
|
81
|
+
.stage span {
|
|
82
|
+
display: block;
|
|
83
|
+
color: var(--yellow);
|
|
84
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
85
|
+
font-size: 12px;
|
|
86
|
+
font-weight: 900;
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
}
|
|
89
|
+
.stage strong { display: block; margin-top: 8px; font-size: 18px; line-height: 1.2; }
|
|
90
|
+
.layout {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: minmax(250px, .8fr) minmax(0, 1.35fr);
|
|
93
|
+
gap: 18px;
|
|
94
|
+
margin-top: 18px;
|
|
95
|
+
}
|
|
96
|
+
.stack { display: grid; gap: 18px; align-content: start; }
|
|
97
|
+
.card {
|
|
98
|
+
border: 4px solid var(--line);
|
|
99
|
+
background: var(--panel);
|
|
100
|
+
box-shadow: 7px 7px 0 var(--line);
|
|
101
|
+
padding: 16px;
|
|
102
|
+
}
|
|
103
|
+
.card h2 {
|
|
104
|
+
margin: 0 0 12px;
|
|
105
|
+
font-size: 19px;
|
|
106
|
+
line-height: 1.2;
|
|
107
|
+
}
|
|
108
|
+
.summary {
|
|
109
|
+
margin: 0;
|
|
110
|
+
color: #223746;
|
|
111
|
+
line-height: 1.6;
|
|
112
|
+
}
|
|
113
|
+
.objectives {
|
|
114
|
+
display: grid;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
margin: 0;
|
|
117
|
+
padding: 0;
|
|
118
|
+
list-style: none;
|
|
119
|
+
}
|
|
120
|
+
.objectives li {
|
|
121
|
+
display: grid;
|
|
122
|
+
grid-template-columns: 26px 1fr;
|
|
123
|
+
gap: 8px;
|
|
124
|
+
align-items: start;
|
|
125
|
+
border: 2px solid var(--line);
|
|
126
|
+
background: var(--paper);
|
|
127
|
+
padding: 9px;
|
|
128
|
+
line-height: 1.45;
|
|
129
|
+
}
|
|
130
|
+
.objectives b {
|
|
131
|
+
display: grid;
|
|
132
|
+
place-items: center;
|
|
133
|
+
width: 24px;
|
|
134
|
+
height: 24px;
|
|
135
|
+
border: 2px solid var(--line);
|
|
136
|
+
background: var(--cyan);
|
|
137
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
}
|
|
140
|
+
.concepts {
|
|
141
|
+
display: grid;
|
|
142
|
+
gap: 10px;
|
|
143
|
+
}
|
|
144
|
+
.concept {
|
|
145
|
+
position: relative;
|
|
146
|
+
display: block;
|
|
147
|
+
width: 100%;
|
|
148
|
+
min-height: 148px;
|
|
149
|
+
border: 0;
|
|
150
|
+
background: transparent;
|
|
151
|
+
color: inherit;
|
|
152
|
+
padding: 0;
|
|
153
|
+
text-align: left;
|
|
154
|
+
perspective: 900px;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
}
|
|
157
|
+
.concept:focus-visible {
|
|
158
|
+
outline: 4px solid var(--cyan);
|
|
159
|
+
outline-offset: 3px;
|
|
160
|
+
}
|
|
161
|
+
.concept-inner {
|
|
162
|
+
position: relative;
|
|
163
|
+
display: grid;
|
|
164
|
+
width: 100%;
|
|
165
|
+
min-height: 148px;
|
|
166
|
+
transform-style: preserve-3d;
|
|
167
|
+
transition: transform .28s ease;
|
|
168
|
+
}
|
|
169
|
+
.concept.is-flipped .concept-inner { transform: rotateY(180deg); }
|
|
170
|
+
.concept-face {
|
|
171
|
+
grid-area: 1 / 1;
|
|
172
|
+
width: 100%;
|
|
173
|
+
min-height: 148px;
|
|
174
|
+
display: grid;
|
|
175
|
+
align-content: start;
|
|
176
|
+
gap: 8px;
|
|
177
|
+
border: 3px solid var(--line);
|
|
178
|
+
background: var(--paper);
|
|
179
|
+
padding: 12px;
|
|
180
|
+
backface-visibility: hidden;
|
|
181
|
+
box-shadow: 4px 4px 0 var(--line);
|
|
182
|
+
}
|
|
183
|
+
.concept-back {
|
|
184
|
+
background: #e8f8ff;
|
|
185
|
+
transform: rotateY(180deg);
|
|
186
|
+
}
|
|
187
|
+
.concept strong { display: block; font-size: 18px; }
|
|
188
|
+
.concept p { margin: 0; line-height: 1.55; }
|
|
189
|
+
.concept small { color: var(--muted); line-height: 1.45; }
|
|
190
|
+
.flip-hint {
|
|
191
|
+
align-self: end;
|
|
192
|
+
width: fit-content;
|
|
193
|
+
border: 2px solid var(--line);
|
|
194
|
+
background: var(--yellow);
|
|
195
|
+
padding: 2px 6px;
|
|
196
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
197
|
+
font-size: 11px;
|
|
198
|
+
font-weight: 900;
|
|
199
|
+
}
|
|
200
|
+
.map {
|
|
201
|
+
position: relative;
|
|
202
|
+
}
|
|
203
|
+
.logic-map {
|
|
204
|
+
display: grid;
|
|
205
|
+
gap: 8px;
|
|
206
|
+
padding: 20px;
|
|
207
|
+
border: 3px solid var(--line);
|
|
208
|
+
background:
|
|
209
|
+
linear-gradient(90deg, rgba(7,17,27,.06) 1px, transparent 1px),
|
|
210
|
+
linear-gradient(0deg, rgba(7,17,27,.06) 1px, transparent 1px),
|
|
211
|
+
rgba(255,255,255,.55);
|
|
212
|
+
background-size: 28px 28px;
|
|
213
|
+
}
|
|
214
|
+
.logic-ascii {
|
|
215
|
+
margin: 0;
|
|
216
|
+
min-height: 260px;
|
|
217
|
+
border: 3px solid var(--line);
|
|
218
|
+
background:
|
|
219
|
+
linear-gradient(90deg, rgba(7,17,27,.06) 1px, transparent 1px),
|
|
220
|
+
linear-gradient(0deg, rgba(7,17,27,.06) 1px, transparent 1px),
|
|
221
|
+
rgba(255,255,255,.65);
|
|
222
|
+
background-size: 28px 28px;
|
|
223
|
+
color: var(--line);
|
|
224
|
+
padding: 20px;
|
|
225
|
+
overflow: auto;
|
|
226
|
+
white-space: pre;
|
|
227
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
228
|
+
font-size: clamp(13px, 1.2vw, 16px);
|
|
229
|
+
font-weight: 800;
|
|
230
|
+
line-height: 1.65;
|
|
231
|
+
}
|
|
232
|
+
.logic-node {
|
|
233
|
+
display: grid;
|
|
234
|
+
gap: 7px;
|
|
235
|
+
width: min(100%, 560px);
|
|
236
|
+
border: 3px solid var(--line);
|
|
237
|
+
background: var(--paper);
|
|
238
|
+
padding: 12px;
|
|
239
|
+
box-shadow: 5px 5px 0 var(--line);
|
|
240
|
+
}
|
|
241
|
+
.logic-node.current { background: var(--yellow); }
|
|
242
|
+
.logic-node.next { background: #dff7ff; }
|
|
243
|
+
.logic-node.prerequisite { background: #f4f0df; }
|
|
244
|
+
.logic-node.warning { background: #ffe0d4; }
|
|
245
|
+
.node-top {
|
|
246
|
+
display: grid;
|
|
247
|
+
gap: 7px;
|
|
248
|
+
align-items: start;
|
|
249
|
+
}
|
|
250
|
+
.logic-node b { font-size: 17px; }
|
|
251
|
+
.tag {
|
|
252
|
+
border: 2px solid var(--line);
|
|
253
|
+
background: var(--deck);
|
|
254
|
+
color: var(--panel);
|
|
255
|
+
padding: 2px 6px;
|
|
256
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
257
|
+
font-size: 11px;
|
|
258
|
+
white-space: nowrap;
|
|
259
|
+
width: fit-content;
|
|
260
|
+
}
|
|
261
|
+
.logic-node p { margin: 0; color: #273b49; line-height: 1.45; }
|
|
262
|
+
.logic-arrow {
|
|
263
|
+
display: grid;
|
|
264
|
+
grid-template-columns: 34px minmax(0, 1fr);
|
|
265
|
+
gap: 8px;
|
|
266
|
+
align-items: center;
|
|
267
|
+
width: min(100%, 560px);
|
|
268
|
+
min-height: 46px;
|
|
269
|
+
color: var(--line);
|
|
270
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
271
|
+
font-weight: 900;
|
|
272
|
+
}
|
|
273
|
+
.logic-arrow-mark {
|
|
274
|
+
display: grid;
|
|
275
|
+
place-items: center;
|
|
276
|
+
font-size: 28px;
|
|
277
|
+
}
|
|
278
|
+
.logic-arrow-label {
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 8px;
|
|
282
|
+
min-width: 0;
|
|
283
|
+
font-size: 12px;
|
|
284
|
+
}
|
|
285
|
+
.logic-arrow-label::before,
|
|
286
|
+
.logic-arrow-label::after {
|
|
287
|
+
content: "";
|
|
288
|
+
height: 3px;
|
|
289
|
+
flex: 1;
|
|
290
|
+
min-width: 12px;
|
|
291
|
+
background: var(--line);
|
|
292
|
+
}
|
|
293
|
+
.logic-arrow-label span {
|
|
294
|
+
border: 2px solid var(--line);
|
|
295
|
+
background: var(--panel);
|
|
296
|
+
color: var(--line);
|
|
297
|
+
padding: 3px 7px;
|
|
298
|
+
white-space: nowrap;
|
|
299
|
+
text-align: center;
|
|
300
|
+
}
|
|
301
|
+
.examples {
|
|
302
|
+
display: grid;
|
|
303
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
304
|
+
gap: 12px;
|
|
305
|
+
}
|
|
306
|
+
.example {
|
|
307
|
+
border: 3px solid var(--line);
|
|
308
|
+
background: var(--paper);
|
|
309
|
+
padding: 12px;
|
|
310
|
+
}
|
|
311
|
+
.example[data-type="nonexample"],
|
|
312
|
+
.example[data-type="counterexample"] { background: #ffe6dc; }
|
|
313
|
+
.example[data-type="worked"] { background: #e8f8ff; }
|
|
314
|
+
.example h3 { margin: 0 0 8px; font-size: 17px; }
|
|
315
|
+
.example p { margin: 0; white-space: pre-wrap; line-height: 1.55; }
|
|
316
|
+
.questions {
|
|
317
|
+
display: grid;
|
|
318
|
+
gap: 12px;
|
|
319
|
+
}
|
|
320
|
+
.question {
|
|
321
|
+
border: 3px solid var(--line);
|
|
322
|
+
background: var(--paper);
|
|
323
|
+
padding: 12px;
|
|
324
|
+
}
|
|
325
|
+
.question h3 {
|
|
326
|
+
margin: 0 0 10px;
|
|
327
|
+
font-size: 16px;
|
|
328
|
+
line-height: 1.35;
|
|
329
|
+
}
|
|
330
|
+
textarea {
|
|
331
|
+
width: 100%;
|
|
332
|
+
min-height: 88px;
|
|
333
|
+
resize: vertical;
|
|
334
|
+
border: 3px solid var(--line);
|
|
335
|
+
background: #fffdf2;
|
|
336
|
+
color: var(--ink);
|
|
337
|
+
padding: 10px;
|
|
338
|
+
line-height: 1.45;
|
|
339
|
+
}
|
|
340
|
+
.option {
|
|
341
|
+
display: flex;
|
|
342
|
+
gap: 8px;
|
|
343
|
+
align-items: flex-start;
|
|
344
|
+
margin: 8px 0;
|
|
345
|
+
line-height: 1.35;
|
|
346
|
+
}
|
|
347
|
+
.option input { margin-top: 3px; }
|
|
348
|
+
.hint {
|
|
349
|
+
margin: 8px 0 0;
|
|
350
|
+
color: var(--muted);
|
|
351
|
+
font-size: 13px;
|
|
352
|
+
line-height: 1.45;
|
|
353
|
+
}
|
|
354
|
+
.next-grid {
|
|
355
|
+
display: grid;
|
|
356
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
357
|
+
gap: 10px;
|
|
358
|
+
}
|
|
359
|
+
.next-item {
|
|
360
|
+
border: 3px solid var(--line);
|
|
361
|
+
background: var(--paper);
|
|
362
|
+
color: inherit;
|
|
363
|
+
padding: 10px;
|
|
364
|
+
text-align: left;
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
box-shadow: 4px 4px 0 var(--line);
|
|
367
|
+
transition: transform .14s ease, background .14s ease, box-shadow .14s ease;
|
|
368
|
+
}
|
|
369
|
+
.next-item:hover,
|
|
370
|
+
.next-item:focus-visible {
|
|
371
|
+
background: #e8f8ff;
|
|
372
|
+
transform: translate(-2px, -2px);
|
|
373
|
+
box-shadow: 6px 6px 0 var(--line);
|
|
374
|
+
}
|
|
375
|
+
.next-item.is-selected {
|
|
376
|
+
background: var(--yellow);
|
|
377
|
+
transform: translate(3px, 3px);
|
|
378
|
+
box-shadow: 1px 1px 0 var(--line);
|
|
379
|
+
}
|
|
380
|
+
.next-item strong { display: block; margin-bottom: 6px; }
|
|
381
|
+
.next-item p { margin: 0; color: #354b59; font-size: 14px; line-height: 1.45; }
|
|
382
|
+
.submit {
|
|
383
|
+
display: grid;
|
|
384
|
+
gap: 10px;
|
|
385
|
+
}
|
|
386
|
+
.submit-row {
|
|
387
|
+
display: grid;
|
|
388
|
+
grid-template-columns: minmax(0, 1fr) 150px;
|
|
389
|
+
gap: 10px;
|
|
390
|
+
}
|
|
391
|
+
.confidence {
|
|
392
|
+
border: 3px solid var(--line);
|
|
393
|
+
background: var(--paper);
|
|
394
|
+
padding: 10px;
|
|
395
|
+
}
|
|
396
|
+
.confidence label {
|
|
397
|
+
display: block;
|
|
398
|
+
font-size: 13px;
|
|
399
|
+
font-weight: 800;
|
|
400
|
+
margin-bottom: 8px;
|
|
401
|
+
}
|
|
402
|
+
.confidence input { width: 100%; }
|
|
403
|
+
.send {
|
|
404
|
+
border: 4px solid var(--line);
|
|
405
|
+
background: var(--yellow);
|
|
406
|
+
color: var(--line);
|
|
407
|
+
padding: 12px 14px;
|
|
408
|
+
font-weight: 900;
|
|
409
|
+
box-shadow: 5px 5px 0 var(--line);
|
|
410
|
+
cursor: pointer;
|
|
411
|
+
}
|
|
412
|
+
.send:disabled {
|
|
413
|
+
cursor: not-allowed;
|
|
414
|
+
opacity: .62;
|
|
415
|
+
box-shadow: none;
|
|
416
|
+
transform: translate(3px, 3px);
|
|
417
|
+
}
|
|
418
|
+
.sent {
|
|
419
|
+
border: 3px solid var(--line);
|
|
420
|
+
background: var(--green);
|
|
421
|
+
padding: 10px;
|
|
422
|
+
display: none;
|
|
423
|
+
line-height: 1.4;
|
|
424
|
+
}
|
|
425
|
+
.sent.is-visible { display: block; }
|
|
426
|
+
.empty {
|
|
427
|
+
border: 3px dashed rgba(7,17,27,.45);
|
|
428
|
+
padding: 12px;
|
|
429
|
+
color: #445663;
|
|
430
|
+
background: rgba(255,255,255,.35);
|
|
431
|
+
}
|
|
432
|
+
@media (max-width: 860px) {
|
|
433
|
+
.hero, .layout, .submit-row { grid-template-columns: 1fr; }
|
|
434
|
+
.stage { min-width: 0; }
|
|
435
|
+
.examples, .next-grid { grid-template-columns: 1fr; }
|
|
436
|
+
.logic-node,
|
|
437
|
+
.logic-arrow { width: 100%; }
|
|
438
|
+
}
|
|
439
|
+
</style>
|
|
440
|
+
</head>
|
|
441
|
+
<body>
|
|
442
|
+
<div class="app">
|
|
443
|
+
<header class="hero">
|
|
444
|
+
<div>
|
|
445
|
+
<div class="eyebrow">LocalWeb Learn</div>
|
|
446
|
+
<h1 id="title">学习面板</h1>
|
|
447
|
+
<p class="subtitle" id="subtitle"></p>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="stage">
|
|
450
|
+
<span>Stage</span>
|
|
451
|
+
<strong id="stage">Concept</strong>
|
|
452
|
+
</div>
|
|
453
|
+
</header>
|
|
454
|
+
|
|
455
|
+
<main class="layout">
|
|
456
|
+
<div class="stack">
|
|
457
|
+
<section class="card">
|
|
458
|
+
<h2>本轮目标</h2>
|
|
459
|
+
<p class="summary" id="summary"></p>
|
|
460
|
+
<ul class="objectives" id="objectives"></ul>
|
|
461
|
+
</section>
|
|
462
|
+
|
|
463
|
+
<section class="card">
|
|
464
|
+
<h2>概念卡</h2>
|
|
465
|
+
<div class="concepts" id="concepts"></div>
|
|
466
|
+
</section>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<div class="stack">
|
|
470
|
+
<section class="card">
|
|
471
|
+
<h2>结构图解</h2>
|
|
472
|
+
<div class="map" id="map"></div>
|
|
473
|
+
</section>
|
|
474
|
+
|
|
475
|
+
<section class="card">
|
|
476
|
+
<h2>例子与边界</h2>
|
|
477
|
+
<div class="examples" id="examples"></div>
|
|
478
|
+
</section>
|
|
479
|
+
|
|
480
|
+
<section class="card">
|
|
481
|
+
<h2>问答训练</h2>
|
|
482
|
+
<div class="questions" id="questions"></div>
|
|
483
|
+
</section>
|
|
484
|
+
|
|
485
|
+
<section class="card">
|
|
486
|
+
<h2>下一步</h2>
|
|
487
|
+
<div class="next-grid" id="next"></div>
|
|
488
|
+
</section>
|
|
489
|
+
|
|
490
|
+
<section class="card submit" id="submit-card">
|
|
491
|
+
<h2>发送到 CLI</h2>
|
|
492
|
+
<div class="submit-row">
|
|
493
|
+
<textarea id="confusion" placeholder="还有哪里不确定?可以留空。"></textarea>
|
|
494
|
+
<div class="confidence">
|
|
495
|
+
<label for="confidence">信心分数 <span id="confidence-value">3</span>/5</label>
|
|
496
|
+
<input id="confidence" type="range" min="1" max="5" value="3">
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
<button class="send" id="send">提交回答</button>
|
|
500
|
+
<div class="sent" id="sent">已发送到 CLI 等待队列。回到终端继续。</div>
|
|
501
|
+
</section>
|
|
502
|
+
</div>
|
|
503
|
+
</main>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<script id="lesson-data" type="application/json">__LOCALWEB_LESSON_DATA__</script>
|
|
507
|
+
<script>
|
|
508
|
+
const lesson = JSON.parse(document.getElementById('lesson-data').textContent);
|
|
509
|
+
const inputId = "__LOCALWEB_INPUT_ID__";
|
|
510
|
+
const interactive = __LOCALWEB_INTERACTIVE__;
|
|
511
|
+
const panelId = "__LOCALWEB_PANEL_ID__";
|
|
512
|
+
let selectedNext = null;
|
|
513
|
+
|
|
514
|
+
function text(value, fallback = '') {
|
|
515
|
+
return value == null ? fallback : String(value);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function esc(value) {
|
|
519
|
+
return text(value).replace(/[&<>"']/g, (ch) => ({
|
|
520
|
+
'&': '&',
|
|
521
|
+
'<': '<',
|
|
522
|
+
'>': '>',
|
|
523
|
+
'"': '"',
|
|
524
|
+
"'": ''',
|
|
525
|
+
}[ch]));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function list(value) {
|
|
529
|
+
return Array.isArray(value) ? value : [];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function byId(id) {
|
|
533
|
+
return document.getElementById(id);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderBasics() {
|
|
537
|
+
byId('title').textContent = text(lesson.title, '学习面板');
|
|
538
|
+
byId('subtitle').textContent = text(lesson.subtitle, '');
|
|
539
|
+
byId('stage').textContent = text(lesson.stage, 'Concept');
|
|
540
|
+
byId('summary').textContent = text(lesson.summary, '暂无摘要。');
|
|
541
|
+
|
|
542
|
+
const objectives = list(lesson.objectives);
|
|
543
|
+
byId('objectives').innerHTML = objectives.length
|
|
544
|
+
? objectives.map((item, index) => `<li><b>${index + 1}</b><span>${esc(item)}</span></li>`).join('')
|
|
545
|
+
: '<li><b>1</b><span>理解当前知识点,并暴露真实卡点。</span></li>';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function renderConcepts() {
|
|
549
|
+
const concepts = list(lesson.concepts);
|
|
550
|
+
byId('concepts').innerHTML = concepts.length
|
|
551
|
+
? concepts.map((item) => {
|
|
552
|
+
const back = item.back || item.detail || item.note || '把这个概念放回当前问题里,检查它承担的是目标、手段还是桥梁。';
|
|
553
|
+
return `<button class="concept" type="button" aria-pressed="false">
|
|
554
|
+
<span class="concept-inner">
|
|
555
|
+
<span class="concept-face concept-front">
|
|
556
|
+
<strong>${esc(item.term || '概念')}</strong>
|
|
557
|
+
<p>${esc(item.definition || '')}</p>
|
|
558
|
+
<span class="flip-hint">点击翻面</span>
|
|
559
|
+
</span>
|
|
560
|
+
<span class="concept-face concept-back">
|
|
561
|
+
<strong>${esc(item.term || '概念')}:补充视角</strong>
|
|
562
|
+
<p>${esc(back)}</p>
|
|
563
|
+
${item.note && item.note !== back ? `<small>${esc(item.note)}</small>` : ''}
|
|
564
|
+
<span class="flip-hint">点击返回</span>
|
|
565
|
+
</span>
|
|
566
|
+
</span>
|
|
567
|
+
</button>`;
|
|
568
|
+
}).join('')
|
|
569
|
+
: '<div class="empty">暂无概念卡。</div>';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function renderMap() {
|
|
573
|
+
const structure = lesson.structure && typeof lesson.structure === 'object' ? lesson.structure : {};
|
|
574
|
+
const diagram = text(structure.diagram).trim();
|
|
575
|
+
if (diagram) {
|
|
576
|
+
byId('map').innerHTML = `<pre class="logic-ascii">${esc(diagram)}</pre>`;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const nodes = list(structure.nodes);
|
|
580
|
+
const edges = list(structure.edges);
|
|
581
|
+
if (!nodes.length) {
|
|
582
|
+
byId('map').innerHTML = '<div class="empty">暂无结构节点。</div>';
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const nodeKey = (node, fallbackId = 'node') => text(node.id || node.label || fallbackId);
|
|
586
|
+
const nodeMap = new Map(nodes.map((node, index) => [
|
|
587
|
+
nodeKey(node, `node-${index}`),
|
|
588
|
+
node,
|
|
589
|
+
]));
|
|
590
|
+
const nodeCard = (node, fallbackId = 'node') => `<article class="logic-node ${esc(node.kind || '')}" data-node-id="${esc(nodeKey(node, fallbackId))}">
|
|
591
|
+
<div class="node-top">
|
|
592
|
+
<b>${esc(node.label || node.id || '节点')}</b>
|
|
593
|
+
<span class="tag">${esc(node.kind || 'node')}</span>
|
|
594
|
+
</div>
|
|
595
|
+
${node.detail ? `<p>${esc(node.detail)}</p>` : ''}
|
|
596
|
+
</article>`;
|
|
597
|
+
let rows = '';
|
|
598
|
+
if (edges.length) {
|
|
599
|
+
const connectedNodeIds = new Set();
|
|
600
|
+
rows = edges.map((edge, index) => {
|
|
601
|
+
const fromId = text(edge.from);
|
|
602
|
+
const toId = text(edge.to);
|
|
603
|
+
if (fromId) connectedNodeIds.add(fromId);
|
|
604
|
+
if (toId) connectedNodeIds.add(toId);
|
|
605
|
+
const fromNode = nodeMap.get(fromId) || { id: fromId, label: fromId, kind: 'support' };
|
|
606
|
+
const toNode = nodeMap.get(toId) || { id: toId, label: toId, kind: 'support' };
|
|
607
|
+
const shouldShowFrom = index === 0 || text(edges[index - 1]?.to) !== fromId;
|
|
608
|
+
return `${shouldShowFrom ? nodeCard(fromNode, fromId) : ''}
|
|
609
|
+
<div class="logic-arrow" data-from="${esc(fromId)}" data-to="${esc(toId)}">
|
|
610
|
+
<div class="logic-arrow-mark">↓</div>
|
|
611
|
+
<div class="logic-arrow-label"><span>${esc(edge.label || '推出')}</span></div>
|
|
612
|
+
</div>
|
|
613
|
+
${nodeCard(toNode, toId)}`;
|
|
614
|
+
}).join('');
|
|
615
|
+
const unconnectedRows = nodes
|
|
616
|
+
.filter((node, index) => !connectedNodeIds.has(nodeKey(node, `node-${index}`)))
|
|
617
|
+
.map((node, index) => nodeCard(node, `unconnected-${index}`))
|
|
618
|
+
.join('');
|
|
619
|
+
rows = `${rows}${unconnectedRows}`;
|
|
620
|
+
} else {
|
|
621
|
+
rows = nodes.map((node, index) => nodeCard(node, `node-${index}`)).join('');
|
|
622
|
+
}
|
|
623
|
+
byId('map').innerHTML = `<div class="logic-map">${rows}</div>`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function renderExamples() {
|
|
627
|
+
const examples = list(lesson.examples);
|
|
628
|
+
byId('examples').innerHTML = examples.length
|
|
629
|
+
? examples.map((item) => `<article class="example" data-type="${esc(item.type || 'example')}">
|
|
630
|
+
<h3>${esc(item.title || '例子')}</h3>
|
|
631
|
+
<p>${esc(item.body || '')}</p>
|
|
632
|
+
</article>`).join('')
|
|
633
|
+
: '<div class="empty">暂无例子。</div>';
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function questionHtml(question, index) {
|
|
637
|
+
const qid = text(question.id, `q${index + 1}`).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
638
|
+
const qtype = text(question.type, 'short');
|
|
639
|
+
let control = '';
|
|
640
|
+
if (qtype === 'choice') {
|
|
641
|
+
const options = list(question.options);
|
|
642
|
+
control = options.length
|
|
643
|
+
? options.map((option) => `<label class="option">
|
|
644
|
+
<input type="radio" name="${esc(qid)}" value="${esc(option.id || option.label || '')}">
|
|
645
|
+
<span>${esc(option.label || option.id || '')}</span>
|
|
646
|
+
</label>`).join('')
|
|
647
|
+
: '<div class="empty">这个选择题还没有选项。</div>';
|
|
648
|
+
} else {
|
|
649
|
+
control = `<textarea data-answer="${esc(qid)}" placeholder="先写你的当前理解,CLI 会负责审计。"></textarea>`;
|
|
650
|
+
}
|
|
651
|
+
return `<article class="question" data-question-id="${esc(qid)}" data-question-type="${esc(qtype)}">
|
|
652
|
+
<h3>${index + 1}. ${esc(question.prompt || '问题')}</h3>
|
|
653
|
+
${control}
|
|
654
|
+
${question.hint ? `<p class="hint">提示:${esc(question.hint)}</p>` : ''}
|
|
655
|
+
</article>`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function renderQuestions() {
|
|
659
|
+
const questions = list(lesson.questions);
|
|
660
|
+
byId('questions').innerHTML = questions.length
|
|
661
|
+
? questions.map(questionHtml).join('')
|
|
662
|
+
: '<div class="empty">暂无问答训练。本面板可作为纯展示材料。</div>';
|
|
663
|
+
if (!interactive) {
|
|
664
|
+
byId('submit-card').style.display = 'none';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function renderNext() {
|
|
669
|
+
const next = list(lesson.next);
|
|
670
|
+
byId('next').innerHTML = next.length
|
|
671
|
+
? next.map((item) => `<button class="next-item" type="button" data-next-id="${esc(item.id || item.label || '')}" aria-pressed="false">
|
|
672
|
+
<strong>${esc(item.label || item.id || '下一步')}</strong>
|
|
673
|
+
<p>${esc(item.description || '')}</p>
|
|
674
|
+
</button>`).join('')
|
|
675
|
+
: '<div class="empty">暂无下一步建议。</div>';
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function bindConceptCards() {
|
|
679
|
+
document.querySelectorAll('.concept').forEach((card) => {
|
|
680
|
+
card.addEventListener('click', () => {
|
|
681
|
+
const flipped = !card.classList.contains('is-flipped');
|
|
682
|
+
card.classList.toggle('is-flipped', flipped);
|
|
683
|
+
card.setAttribute('aria-pressed', flipped ? 'true' : 'false');
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function bindNextCards() {
|
|
689
|
+
byId('next').addEventListener('click', (event) => {
|
|
690
|
+
const card = event.target.closest('.next-item');
|
|
691
|
+
if (!card) return;
|
|
692
|
+
document.querySelectorAll('.next-item').forEach((item) => {
|
|
693
|
+
item.classList.remove('is-selected');
|
|
694
|
+
item.setAttribute('aria-pressed', 'false');
|
|
695
|
+
});
|
|
696
|
+
card.classList.add('is-selected');
|
|
697
|
+
card.setAttribute('aria-pressed', 'true');
|
|
698
|
+
selectedNext = {
|
|
699
|
+
id: card.dataset.nextId || '',
|
|
700
|
+
label: card.querySelector('strong')?.textContent || '',
|
|
701
|
+
description: card.querySelector('p')?.textContent || '',
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function collectAnswers() {
|
|
707
|
+
const rows = [];
|
|
708
|
+
document.querySelectorAll('.question').forEach((card) => {
|
|
709
|
+
const id = card.dataset.questionId || '';
|
|
710
|
+
const type = card.dataset.questionType || 'short';
|
|
711
|
+
const prompt = card.querySelector('h3')?.textContent || id;
|
|
712
|
+
let answer = '';
|
|
713
|
+
if (type === 'choice') {
|
|
714
|
+
const checked = card.querySelector('input[type="radio"]:checked');
|
|
715
|
+
answer = checked ? checked.value : '';
|
|
716
|
+
} else {
|
|
717
|
+
answer = card.querySelector('textarea')?.value.trim() || '';
|
|
718
|
+
}
|
|
719
|
+
rows.push({ id, type, prompt, answer });
|
|
720
|
+
});
|
|
721
|
+
return rows;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function markdownPayload() {
|
|
725
|
+
const answers = collectAnswers();
|
|
726
|
+
const confidence = byId('confidence').value;
|
|
727
|
+
const confusion = byId('confusion').value.trim();
|
|
728
|
+
const lines = [
|
|
729
|
+
`# Learn 回答:${text(lesson.title, '学习面板')}`,
|
|
730
|
+
'',
|
|
731
|
+
`- lesson_id: ${text(lesson.id, '')}`,
|
|
732
|
+
`- stage: ${text(lesson.stage, '')}`,
|
|
733
|
+
`- confidence: ${confidence}/5`,
|
|
734
|
+
`- selected_next: ${selectedNext ? selectedNext.id : ''}`,
|
|
735
|
+
'',
|
|
736
|
+
'## Answers',
|
|
737
|
+
];
|
|
738
|
+
answers.forEach((item) => {
|
|
739
|
+
lines.push('', `### ${item.id}`, '', `- type: ${item.type}`, `- prompt: ${item.prompt}`, '', item.answer ? item.answer : '_未作答_');
|
|
740
|
+
});
|
|
741
|
+
lines.push('', '## Selected Next', '', selectedNext ? `- ${selectedNext.label}: ${selectedNext.description}` : '_未选择_');
|
|
742
|
+
lines.push('', '## Confusion', '', confusion ? confusion : '_无_');
|
|
743
|
+
return lines.join('\n');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function bindSubmit() {
|
|
747
|
+
const confidence = byId('confidence');
|
|
748
|
+
confidence.addEventListener('input', () => {
|
|
749
|
+
byId('confidence-value').textContent = confidence.value;
|
|
750
|
+
});
|
|
751
|
+
byId('send').addEventListener('click', () => {
|
|
752
|
+
const payload = markdownPayload();
|
|
753
|
+
window.parent.postMessage({
|
|
754
|
+
localweb: true,
|
|
755
|
+
type: 'panel_input',
|
|
756
|
+
input_id: inputId,
|
|
757
|
+
text: payload,
|
|
758
|
+
label: 'learn answers',
|
|
759
|
+
panel_id: panelId,
|
|
760
|
+
meta: {
|
|
761
|
+
mode: 'learn',
|
|
762
|
+
lesson_id: text(lesson.id, ''),
|
|
763
|
+
stage: text(lesson.stage, ''),
|
|
764
|
+
},
|
|
765
|
+
}, '*');
|
|
766
|
+
byId('send').disabled = true;
|
|
767
|
+
byId('sent').classList.add('is-visible');
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
renderBasics();
|
|
772
|
+
renderConcepts();
|
|
773
|
+
renderMap();
|
|
774
|
+
renderExamples();
|
|
775
|
+
renderQuestions();
|
|
776
|
+
renderNext();
|
|
777
|
+
bindConceptCards();
|
|
778
|
+
bindNextCards();
|
|
779
|
+
bindSubmit();
|
|
780
|
+
</script>
|
|
781
|
+
</body>
|
|
782
|
+
</html>
|