icoa-cli 2.19.357 → 2.19.359
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/dist/commands/ai4ctf.js +1 -1
- package/dist/commands/ctf.js +1 -787
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/demo2.js +1 -1502
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/exam.js +1 -1
- package/dist/commands/files.js +1 -59
- package/dist/commands/lang.js +1 -202
- package/dist/commands/log.js +1 -171
- package/dist/commands/shell.js +1 -151
- package/dist/commands/sim.js +1 -389
- package/dist/index.js +1 -355
- package/dist/lib/access.js +1 -184
- package/dist/lib/aienv.js +1 -205
- package/dist/lib/arena-submit.js +1 -21
- package/dist/lib/banner.js +1 -31
- package/dist/lib/budget.js +1 -6
- package/dist/lib/challenge-dir.js +1 -16
- package/dist/lib/colors.js +1 -17
- package/dist/lib/comms.js +1 -212
- package/dist/lib/config.js +1 -93
- package/dist/lib/countdown.js +1 -43
- package/dist/lib/country-lang.js +1 -39
- package/dist/lib/ctfd-client.js +1 -417
- package/dist/lib/demo-exam.js +1 -478
- package/dist/lib/demo-flags.js +1 -27
- package/dist/lib/demo-stats.js +1 -62
- package/dist/lib/demo2-progress.js +1 -102
- package/dist/lib/docker-probe.js +1 -118
- package/dist/lib/editor-spawn.js +1 -53
- package/dist/lib/exam-client.js +1 -54
- package/dist/lib/exam-sandbox.js +1 -201
- package/dist/lib/exam-setup.js +1 -36
- package/dist/lib/exam-state.js +1 -273
- package/dist/lib/gemini.js +1 -247
- package/dist/lib/i18n.js +1 -302
- package/dist/lib/integrity-snapshot.js +1 -88
- package/dist/lib/interactive-spawn.js +1 -55
- package/dist/lib/ipynb-input.js +1 -65
- package/dist/lib/kernel-protocol.js +1 -88
- package/dist/lib/kernel.js +2 -146
- package/dist/lib/learn-curricula.js +1 -309
- package/dist/lib/learn-i18n.js +1 -184
- package/dist/lib/learn-input.js +1 -101
- package/dist/lib/learn-render.js +1 -863
- package/dist/lib/learn-state.js +1 -103
- package/dist/lib/log-sync.js +1 -155
- package/dist/lib/logger.js +1 -49
- package/dist/lib/main-rl.js +1 -7
- package/dist/lib/menu-nav.js +1 -105
- package/dist/lib/notebook-doc.js +1 -137
- package/dist/lib/open-file.js +1 -55
- package/dist/lib/paper-upgrade.js +1 -119
- package/dist/lib/platform.js +1 -99
- package/dist/lib/render-card.js +1 -112
- package/dist/lib/repl-asker.js +1 -67
- package/dist/lib/sample-runner.js +1 -227
- package/dist/lib/sandbox.js +1 -144
- package/dist/lib/shell-split.js +1 -69
- package/dist/lib/sim-cooldown.js +1 -75
- package/dist/lib/theme.js +1 -119
- package/dist/lib/token-format.js +1 -74
- package/dist/lib/tool-man.js +1 -418
- package/dist/lib/toolset-hash.js +1 -48
- package/dist/lib/translation.js +1 -80
- package/dist/lib/translations-fetcher.js +1 -95
- package/dist/lib/ui.js +1 -99
- package/dist/lib/update-check.js +1 -114
- package/dist/lib/version.js +1 -24
- package/dist/postinstall.js +1 -48
- package/dist/repl.js +1 -2391
- package/dist/types/index.js +1 -63
- package/package.json +1 -1
package/dist/lib/learn-render.js
CHANGED
|
@@ -1,863 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Learn-mode rendering helpers.
|
|
3
|
-
* All output uses chalk (theme.ts sets black background — never use bare console.log).
|
|
4
|
-
*
|
|
5
|
-
* Border style: three-sided frame (top / bottom / left). No right border —
|
|
6
|
-
* variable-length lines (especially with ANSI colour codes) can't be padded
|
|
7
|
-
* reliably across terminals, so any right edge breaks alignment. Top and
|
|
8
|
-
* bottom are open-ended horizontal rules; left is a continuous vertical bar.
|
|
9
|
-
*
|
|
10
|
-
* i18n: `localized(card, lang)` from learn-curricula.ts applies `_zh` field
|
|
11
|
-
* overlays on each card. `t(key, lang)` from learn-i18n.ts handles chrome
|
|
12
|
-
* (welcome screen, prompts, dashboard).
|
|
13
|
-
*/
|
|
14
|
-
import chalk from 'chalk';
|
|
15
|
-
import stringWidth from 'string-width';
|
|
16
|
-
import { localized } from './learn-curricula.js';
|
|
17
|
-
import { t } from './learn-i18n.js';
|
|
18
|
-
import { getConfig } from './config.js';
|
|
19
|
-
import { icoaBannerLines } from './banner.js';
|
|
20
|
-
import { renderCharts, detectCaps } from './render-card.js';
|
|
21
|
-
function lang() {
|
|
22
|
-
return (getConfig().language || 'en').toLowerCase();
|
|
23
|
-
}
|
|
24
|
-
const W = 64; // content width — drives top/bottom rule length only
|
|
25
|
-
function rule(char = '─') {
|
|
26
|
-
return char.repeat(W + 2);
|
|
27
|
-
}
|
|
28
|
-
function progressBar(done, total, width = 20) {
|
|
29
|
-
const ratio = total > 0 ? done / total : 0;
|
|
30
|
-
const filled = Math.floor(ratio * width);
|
|
31
|
-
const empty = width - filled;
|
|
32
|
-
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
33
|
-
const pct = (ratio * 100).toFixed(1);
|
|
34
|
-
return `${bar} ${pct}%`;
|
|
35
|
-
}
|
|
36
|
-
function fireStreak(days) {
|
|
37
|
-
if (days >= 30)
|
|
38
|
-
return chalk.red('🔥🔥🔥 ' + days + ' days');
|
|
39
|
-
if (days >= 7)
|
|
40
|
-
return chalk.yellow('🔥 ' + days + ' days');
|
|
41
|
-
if (days >= 1)
|
|
42
|
-
return chalk.gray('· ' + days + ' day' + (days > 1 ? 's' : ''));
|
|
43
|
-
return chalk.gray('—');
|
|
44
|
-
}
|
|
45
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
46
|
-
// Welcome screen
|
|
47
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
48
|
-
export function renderWelcome(curriculum, state, isNewSession) {
|
|
49
|
-
const L = lang();
|
|
50
|
-
const done = state.cardsCompleted.length;
|
|
51
|
-
const total = curriculum.totalCards;
|
|
52
|
-
const remaining = total - done;
|
|
53
|
-
const currentModule = curriculum.modules.find((m) => state.currentCard >= m.cardRange[0] && state.currentCard <= m.cardRange[1]);
|
|
54
|
-
// Title + "free demo" line are derived from the curriculum itself, never
|
|
55
|
-
// hardcoded — a fixed "Embodied AI Security Academy / free demo" banner was
|
|
56
|
-
// wrong for every paid track (ai4ctf/ioai/…). Source of truth = curriculum.name
|
|
57
|
-
// + an id-based demo check. (Same lesson as the exam header-from-truth rule.)
|
|
58
|
-
const isDemo = /demo/i.test(curriculum.id);
|
|
59
|
-
console.log();
|
|
60
|
-
console.log(chalk.cyan(' ╭' + rule('═')));
|
|
61
|
-
console.log(chalk.cyan(' ║'));
|
|
62
|
-
console.log(chalk.cyan(' ║ ') + chalk.bold.white(' ' + (curriculum.name?.trim() || t('academy_title', L))));
|
|
63
|
-
console.log(chalk.cyan(' ║'));
|
|
64
|
-
if (isNewSession) {
|
|
65
|
-
console.log(chalk.cyan(' ║ ') + chalk.white(' ' + t(isDemo ? 'welcome_new_demo' : 'welcome_new', L)));
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
const lastDate = new Date(state.lastSeenAt);
|
|
69
|
-
const hoursAgo = Math.floor((Date.now() - lastDate.getTime()) / 3600000);
|
|
70
|
-
const since = hoursAgo < 1
|
|
71
|
-
? t('just_now', L)
|
|
72
|
-
: hoursAgo < 24
|
|
73
|
-
? `${hoursAgo}${t('ago_hours', L)}`
|
|
74
|
-
: `${Math.floor(hoursAgo / 24)}${t('ago_days', L)}`;
|
|
75
|
-
console.log(chalk.cyan(' ║ ') + chalk.white(` ${t('welcome_back', L)} ${since}`));
|
|
76
|
-
}
|
|
77
|
-
console.log(chalk.cyan(' ║'));
|
|
78
|
-
const moduleName = currentModule ? `${currentModule.number}. ${currentModule.name}` : '—';
|
|
79
|
-
console.log(chalk.cyan(' ║ ') + ' ' + chalk.gray(t('module', L).padEnd(11)) + chalk.white(moduleName));
|
|
80
|
-
console.log(chalk.cyan(' ║ ') +
|
|
81
|
-
' ' +
|
|
82
|
-
chalk.gray(t('progress', L).padEnd(11)) +
|
|
83
|
-
progressBar(done, total) +
|
|
84
|
-
chalk.gray(` (${done}/${total})`));
|
|
85
|
-
console.log(chalk.cyan(' ║ ') + ' ' + chalk.gray(t('streak', L).padEnd(11)) + fireStreak(state.streakDays));
|
|
86
|
-
if (remaining > 0 && state.currentCard <= total) {
|
|
87
|
-
console.log(chalk.cyan(' ║ ') +
|
|
88
|
-
' ' +
|
|
89
|
-
chalk.gray(t('next_card', L).padEnd(11)) +
|
|
90
|
-
chalk.white(`#${state.currentCard} ${t('of', L)} ${total}`));
|
|
91
|
-
}
|
|
92
|
-
console.log(chalk.cyan(' ║'));
|
|
93
|
-
console.log(chalk.cyan(' ╰' + rule('═')));
|
|
94
|
-
console.log();
|
|
95
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
96
|
-
const continueLabel = done === 0 ? t('continue_start', L) : `${t('continue_resume', L)} ${state.currentCard}`;
|
|
97
|
-
console.log(chalk.bold.green(' continue') + chalk.gray(' ' + continueLabel));
|
|
98
|
-
console.log(chalk.yellow(' status') + chalk.gray(' ' + t('status_full', L)));
|
|
99
|
-
if (state.bookmarks.length > 0)
|
|
100
|
-
console.log(chalk.yellow(' bookmarks') + chalk.gray(` ${state.bookmarks.length} ${t('bookmarks_desc', L)}`));
|
|
101
|
-
console.log(chalk.gray(' quit') + chalk.gray(' ' + t('quit_desc', L)));
|
|
102
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
103
|
-
console.log();
|
|
104
|
-
}
|
|
105
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
106
|
-
// Card rendering — varies by type
|
|
107
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
108
|
-
function cardHeader(card, curriculum) {
|
|
109
|
-
const L = lang();
|
|
110
|
-
const moduleNum = card.module;
|
|
111
|
-
const module = curriculum.modules.find((m) => m.number === moduleNum);
|
|
112
|
-
const moduleName = module ? module.name : t('unknown_module', L);
|
|
113
|
-
const moduleLabel = t('module', L).replace(':', '').trim();
|
|
114
|
-
const header = `${t('card', L)} ${card.number} / ${curriculum.totalCards} · ${moduleLabel} ${moduleNum} · ${moduleName}`;
|
|
115
|
-
console.log();
|
|
116
|
-
console.log(chalk.cyan(' ╭─ ') + chalk.bold.white(header) + ' ' + chalk.cyan('─'.repeat(Math.max(0, W - header.length - 4))));
|
|
117
|
-
}
|
|
118
|
-
function cardFooter() {
|
|
119
|
-
console.log(chalk.cyan(' ╰' + rule()));
|
|
120
|
-
console.log();
|
|
121
|
-
}
|
|
122
|
-
function bodyLine(text) {
|
|
123
|
-
console.log(chalk.cyan(' │ ') + chalk.white(text));
|
|
124
|
-
}
|
|
125
|
-
function emptyLine() {
|
|
126
|
-
console.log(chalk.cyan(' │'));
|
|
127
|
-
}
|
|
128
|
-
// Box-drawing characters used in ASCII diagrams. Arrows (→ ← ↑ ↓) are
|
|
129
|
-
// excluded — they appear inline in 110+ ctf4eai-360 prose paragraphs.
|
|
130
|
-
const BOX_DRAWING = /[┌┐└┘├┤┬┴┼─│╔╗╚╝═║╠╣╦╩╬▓▒░█]/;
|
|
131
|
-
function isPreformatted(line) {
|
|
132
|
-
return /^(?: {4}|\t)/.test(line) || BOX_DRAWING.test(line) || / {3,}/.test(line);
|
|
133
|
-
}
|
|
134
|
-
function tokenizeForWrap(text) {
|
|
135
|
-
const toks = [];
|
|
136
|
-
const pushText = (str) => {
|
|
137
|
-
let word = '';
|
|
138
|
-
const flush = () => {
|
|
139
|
-
if (word) {
|
|
140
|
-
toks.push({ t: 'atom', s: word });
|
|
141
|
-
word = '';
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
for (const ch of str) {
|
|
145
|
-
if (ch === ' ') {
|
|
146
|
-
flush();
|
|
147
|
-
toks.push({ t: 'space' });
|
|
148
|
-
}
|
|
149
|
-
else if (stringWidth(ch) === 2) {
|
|
150
|
-
flush();
|
|
151
|
-
toks.push({ t: 'wide', s: ch }); // CJK / emoji — breakable per char
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
word += ch;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
flush();
|
|
158
|
-
};
|
|
159
|
-
const re = /`[^`]*`/g;
|
|
160
|
-
let last = 0;
|
|
161
|
-
let m;
|
|
162
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop idiom
|
|
163
|
-
while ((m = re.exec(text)) !== null) {
|
|
164
|
-
pushText(text.slice(last, m.index));
|
|
165
|
-
toks.push({ t: 'code', s: m[0] }); // backticks kept → atomic during wrap
|
|
166
|
-
last = m.index + m[0].length;
|
|
167
|
-
}
|
|
168
|
-
pushText(text.slice(last));
|
|
169
|
-
return toks;
|
|
170
|
-
}
|
|
171
|
-
function wrapMixed(text, width) {
|
|
172
|
-
const lines = [];
|
|
173
|
-
let cur = '';
|
|
174
|
-
let curW = 0;
|
|
175
|
-
let pendSpace = false;
|
|
176
|
-
for (const tok of tokenizeForWrap(text)) {
|
|
177
|
-
if (tok.t === 'space') {
|
|
178
|
-
if (cur !== '')
|
|
179
|
-
pendSpace = true;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const piece = tok.s;
|
|
183
|
-
const w = stringWidth(tok.t === 'code' ? piece.replace(/`/g, '') : piece);
|
|
184
|
-
const sep = pendSpace ? 1 : 0;
|
|
185
|
-
// Short trailing punctuation sticks to the current line (avoids orphaning a
|
|
186
|
-
// lone "." onto the next line after an over-wide code span).
|
|
187
|
-
const isPunct = tok.t === 'atom' && piece.length <= 2 && /^[.,;:!?)\]}…'"]+$/.test(piece);
|
|
188
|
-
if (cur !== '' && !isPunct && curW + sep + w > width) {
|
|
189
|
-
lines.push(cur);
|
|
190
|
-
cur = piece;
|
|
191
|
-
curW = w;
|
|
192
|
-
pendSpace = false;
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
if (pendSpace) {
|
|
196
|
-
cur += ' ';
|
|
197
|
-
curW += 1;
|
|
198
|
-
pendSpace = false;
|
|
199
|
-
}
|
|
200
|
-
cur += piece;
|
|
201
|
-
curW += w;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (cur)
|
|
205
|
-
lines.push(cur);
|
|
206
|
-
return lines;
|
|
207
|
-
}
|
|
208
|
-
// Pitfall / warning cue phrases → bold.yellow (#3, "caution" half). Bilingual,
|
|
209
|
-
// bounded set. Ambiguous words (Warning / 注意 / …) require a trailing colon to
|
|
210
|
-
// avoid lighting up ordinary prose; strong phrases (Pitfall / Common bug) don't.
|
|
211
|
-
const PITFALL_RE = /\b(?:Common bug|Common mistake|Common error|Pitfall|Gotcha|Footgun)\b[::]?|\b(?:Warning|Caution)\b[::]|(?:常见错误|常见陷阱|常见误区|陷阱|注意|警告|误区)[::]/gi;
|
|
212
|
-
function emphasize(text, bold) {
|
|
213
|
-
const base = bold ? chalk.bold.white : chalk.white;
|
|
214
|
-
let out = '';
|
|
215
|
-
let last = 0;
|
|
216
|
-
PITFALL_RE.lastIndex = 0;
|
|
217
|
-
let m;
|
|
218
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop idiom
|
|
219
|
-
while ((m = PITFALL_RE.exec(text)) !== null) {
|
|
220
|
-
if (m.index > last)
|
|
221
|
-
out += base(text.slice(last, m.index));
|
|
222
|
-
out += chalk.bold.yellow(m[0]);
|
|
223
|
-
last = m.index + m[0].length;
|
|
224
|
-
}
|
|
225
|
-
if (last < text.length)
|
|
226
|
-
out += base(text.slice(last));
|
|
227
|
-
return out;
|
|
228
|
-
}
|
|
229
|
-
// Strip backticks → inline code cyan; prose via emphasize() (white / bold.white
|
|
230
|
-
// for the lead sentence #3, with pitfall cues lifted to bold.yellow).
|
|
231
|
-
function colorizeInline(s, bold = false) {
|
|
232
|
-
let out = '';
|
|
233
|
-
let last = 0;
|
|
234
|
-
const re = /`([^`]*)`/g;
|
|
235
|
-
let m;
|
|
236
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop idiom
|
|
237
|
-
while ((m = re.exec(s)) !== null) {
|
|
238
|
-
if (m.index > last)
|
|
239
|
-
out += emphasize(s.slice(last, m.index), bold);
|
|
240
|
-
out += chalk.cyan(m[1]);
|
|
241
|
-
last = m.index + m[0].length;
|
|
242
|
-
}
|
|
243
|
-
if (last < s.length)
|
|
244
|
-
out += emphasize(s.slice(last), bold);
|
|
245
|
-
return out;
|
|
246
|
-
}
|
|
247
|
-
// Render one body paragraph: ```fenced``` → monospace block (no wrap, cyan);
|
|
248
|
-
// inline `code` → cyan, never split; ASCII diagrams → passthrough; prose → wrap.
|
|
249
|
-
// `bold` paints prose bold.white (used for the lead sentence — #3).
|
|
250
|
-
function renderBodyParagraph(para, bold = false) {
|
|
251
|
-
let inFence = false;
|
|
252
|
-
for (const raw of para.split('\n')) {
|
|
253
|
-
if (/^\s*```/.test(raw)) {
|
|
254
|
-
inFence = !inFence;
|
|
255
|
-
continue; // swallow the fence marker line itself
|
|
256
|
-
}
|
|
257
|
-
if (inFence) {
|
|
258
|
-
console.log(chalk.cyan(' │ ') + chalk.cyan(` ${raw}`));
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (raw === '') {
|
|
262
|
-
emptyLine();
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
if (isPreformatted(raw)) {
|
|
266
|
-
console.log(chalk.cyan(' │ ') + chalk.white(raw));
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
for (const ln of wrapMixed(raw, W - 4)) {
|
|
270
|
-
console.log(chalk.cyan(' │ ') + ` ${colorizeInline(ln, bold)}`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
function wrapText(text, width) {
|
|
275
|
-
const out = [];
|
|
276
|
-
for (const line of text.split('\n')) {
|
|
277
|
-
if (line === '') {
|
|
278
|
-
out.push('');
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (isPreformatted(line)) {
|
|
282
|
-
out.push(line);
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
let buf = '';
|
|
286
|
-
for (const word of line.split(' ')) {
|
|
287
|
-
if ((buf + ' ' + word).trim().length > width) {
|
|
288
|
-
out.push(buf.trim());
|
|
289
|
-
buf = word;
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
buf = (buf + ' ' + word).trim();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (buf)
|
|
296
|
-
out.push(buf);
|
|
297
|
-
}
|
|
298
|
-
return out;
|
|
299
|
-
}
|
|
300
|
-
// Green BLUF "key point" band — sandwiched in two ASCII-dash dividers, placed
|
|
301
|
-
// after the visual slot and before the body. Classic black-green CTF look:
|
|
302
|
-
// green dashes + bold-green sentence. Dashes are ASCII '-' (NOT U+2500, which
|
|
303
|
-
// is EAW-ambiguous → CJK width break). The one-liner is an INDEPENDENT
|
|
304
|
-
// proposition (never spoils the y/n). See docs/learn-card-visuals.md.
|
|
305
|
-
function renderKeyPointBand(keyPoint, cardNum, total) {
|
|
306
|
-
const isZh = lang().startsWith('zh');
|
|
307
|
-
const cols = detectCaps().cols;
|
|
308
|
-
const dashes = '-'.repeat(Math.min(W, Math.max(24, cols - 6)));
|
|
309
|
-
const label = isZh ? '要点' : 'KEY POINT';
|
|
310
|
-
const cardLabel = isZh ? `第 ${cardNum} / ${total} 张` : `Card ${cardNum} / ${total}`;
|
|
311
|
-
console.log(chalk.cyan(' │ ') + chalk.green(dashes));
|
|
312
|
-
console.log(chalk.cyan(' │ ') + chalk.green(`${label} · ${cardLabel}`));
|
|
313
|
-
for (const ln of wrapMixed(keyPoint, W - 4)) {
|
|
314
|
-
console.log(chalk.cyan(' │ ') + chalk.bold.green(' ' + ln));
|
|
315
|
-
}
|
|
316
|
-
console.log(chalk.cyan(' │ ') + chalk.green(dashes));
|
|
317
|
-
emptyLine();
|
|
318
|
-
}
|
|
319
|
-
// Poka-yoke action footer for knowledge cards — persistent guidance every
|
|
320
|
-
// screen (Newbie Path Invariant). Adds the `e` report channel + `b` back +
|
|
321
|
-
// `card N` jump, plus the 防呆 one-liner clarifying `e` is not an answer.
|
|
322
|
-
function renderCardActionFooter(card, curriculum) {
|
|
323
|
-
const isZh = lang().startsWith('zh');
|
|
324
|
-
const cols = detectCaps().cols;
|
|
325
|
-
const dashes = '-'.repeat(Math.min(W, Math.max(24, cols - 6)));
|
|
326
|
-
const sep = chalk.gray(' ');
|
|
327
|
-
const parts = [];
|
|
328
|
-
if (card.check) {
|
|
329
|
-
parts.push(chalk.bold.green('[y]') + chalk.gray(isZh ? ' 是' : ' yes'));
|
|
330
|
-
parts.push(chalk.bold.green('[n]') + chalk.gray(isZh ? ' 否' : ' no'));
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
parts.push(chalk.bold.green('[ok]') + chalk.gray(isZh ? ' 继续' : ' next'));
|
|
334
|
-
}
|
|
335
|
-
parts.push(chalk.bold.yellow('[e]') + chalk.gray(isZh ? ' 报告问题' : ' report'));
|
|
336
|
-
parts.push(chalk.bold.cyan('[b]') + chalk.gray(isZh ? ' 上一张' : ' back'));
|
|
337
|
-
parts.push(chalk.bold.cyan('[card N]') + chalk.gray(isZh ? ' 跳转' : ' jump'));
|
|
338
|
-
parts.push(chalk.gray('[quit]') + chalk.gray(isZh ? ' 退出' : ' quit'));
|
|
339
|
-
console.log(chalk.gray(' ' + dashes));
|
|
340
|
-
console.log(chalk.gray(' ' +
|
|
341
|
-
(isZh ? `第 ${card.number} / ${curriculum.totalCards} 张` : `Card ${card.number} / ${curriculum.totalCards}`)));
|
|
342
|
-
console.log(' ' + parts.join(sep));
|
|
343
|
-
console.log(chalk.gray(' ' + dashes));
|
|
344
|
-
console.log(chalk.gray(' ' +
|
|
345
|
-
(isZh
|
|
346
|
-
? 'e = 报告本卡错误,不算作答;提交后回到本题继续'
|
|
347
|
-
: 'e = report a problem with this card (not an answer); you return here after')));
|
|
348
|
-
console.log(chalk.gray(' ' + (isZh ? '更多: bookmark · status · lang zh/en' : 'more: bookmark · status · lang zh/en')));
|
|
349
|
-
console.log();
|
|
350
|
-
}
|
|
351
|
-
export function renderKnowledgeCard(card, curriculum) {
|
|
352
|
-
const L = lang();
|
|
353
|
-
const c = localized(card, L);
|
|
354
|
-
cardHeader(c, curriculum);
|
|
355
|
-
emptyLine();
|
|
356
|
-
const titleLines = wrapMixed(c.title, W - 4);
|
|
357
|
-
for (const tl of titleLines)
|
|
358
|
-
bodyLine(chalk.bold.yellow(tl));
|
|
359
|
-
bodyLine(chalk.gray('─'.repeat(Math.min(Math.max(...titleLines.map((l) => stringWidth(l))), W))));
|
|
360
|
-
// Slot order: title → ≤1 visual → green key-point band → body.
|
|
361
|
-
// chart_ids is on the canonical card; localized() preserves it because the
|
|
362
|
-
// _zh overlay only carries title/body/icoaConnection/checkStatement/key_point.
|
|
363
|
-
// MAX-ONE: only the first visual id is rendered (docs/learn-card-visuals.md).
|
|
364
|
-
const hasVisual = !!(card.chart_ids && card.chart_ids.length > 0);
|
|
365
|
-
if (hasVisual) {
|
|
366
|
-
renderCharts(card.chart_ids.slice(0, 1), curriculum.charts, detectCaps());
|
|
367
|
-
}
|
|
368
|
-
if (c.key_point && c.key_point.trim() !== '') {
|
|
369
|
-
if (!hasVisual)
|
|
370
|
-
emptyLine(); // visual already left a trailing blank line
|
|
371
|
-
renderKeyPointBand(c.key_point, c.number, curriculum.totalCards);
|
|
372
|
-
}
|
|
373
|
-
else if (!hasVisual) {
|
|
374
|
-
emptyLine();
|
|
375
|
-
}
|
|
376
|
-
// Drop empty / whitespace-only paragraphs: some cards (e.g. Phase 8) author
|
|
377
|
-
// body arrays with "" elements as separators, which double-space against the
|
|
378
|
-
// emptyLine() the loop already adds between paragraphs (looked like 3 blanks).
|
|
379
|
-
// Filtering first also keeps idx===0 = the first real paragraph (lead bold).
|
|
380
|
-
c.body
|
|
381
|
-
.filter((p) => p != null && p.trim() !== '')
|
|
382
|
-
.forEach((para, idx) => {
|
|
383
|
-
// #3: bold the lead sentence of the first paragraph (the card's takeaway).
|
|
384
|
-
// Sentence end = . ! ? followed by space/EOL (skips decimals like 0.75 and
|
|
385
|
-
// dotted code like np.ndarray) or any CJK terminator 。!?.
|
|
386
|
-
const lead = idx === 0 ? para.match(/^([\s\S]*?(?:[.!?](?=\s|$)|[。!?]))(\s[\s\S]*)?$/) : null;
|
|
387
|
-
if (lead && lead[1].trim().length <= 180) {
|
|
388
|
-
renderBodyParagraph(lead[1], true);
|
|
389
|
-
const rest = lead[2]?.trimStart();
|
|
390
|
-
if (rest)
|
|
391
|
-
renderBodyParagraph(rest, false);
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
renderBodyParagraph(para, false);
|
|
395
|
-
}
|
|
396
|
-
emptyLine();
|
|
397
|
-
});
|
|
398
|
-
if (c.icoaConnection) {
|
|
399
|
-
bodyLine(chalk.magenta(' ' + t('icoa_connection', L)));
|
|
400
|
-
bodyLine(chalk.gray(' ' + '─'.repeat(20)));
|
|
401
|
-
for (const ln of wrapMixed(c.icoaConnection, W - 4)) {
|
|
402
|
-
bodyLine(' ' + chalk.magenta(ln));
|
|
403
|
-
}
|
|
404
|
-
emptyLine();
|
|
405
|
-
}
|
|
406
|
-
// Optional comprehension check — one-keystroke y/n. Pulls the zh override
|
|
407
|
-
// from card._zh when in zh locale (localized() doesn't reach nested fields).
|
|
408
|
-
// Supports BOTH the flat seed shape (`_zh.checkStatement`) AND the raw
|
|
409
|
-
// Claude shape (`_zh.check.statement`) so it works pre- or post-flatten.
|
|
410
|
-
if (card.check) {
|
|
411
|
-
const isZh = L.startsWith('zh');
|
|
412
|
-
const zhStatement = card._zh?.checkStatement ?? card._zh?.check?.statement;
|
|
413
|
-
const statement = isZh ? (zhStatement ?? card.check.statement) : card.check.statement;
|
|
414
|
-
bodyLine(chalk.cyan(' 🤔 ' + (isZh ? '请回答 (输 y 或 n):' : 'Quick comprehension check (type y or n):')));
|
|
415
|
-
bodyLine(chalk.gray(' ' + '─'.repeat(20)));
|
|
416
|
-
for (const ln of wrapMixed(statement, W - 4)) {
|
|
417
|
-
bodyLine(' ' + chalk.white(ln));
|
|
418
|
-
}
|
|
419
|
-
emptyLine();
|
|
420
|
-
}
|
|
421
|
-
cardFooter();
|
|
422
|
-
renderCardActionFooter(card, curriculum);
|
|
423
|
-
}
|
|
424
|
-
// Renders the post-check feedback line. Called from learn.ts after y/n.
|
|
425
|
-
//
|
|
426
|
-
// BASIC tier (default): correct → ✓; wrong → brief ✗ ack ONLY — no answer
|
|
427
|
-
// reveal, no explanation, no pause. Both auto-advance (the card is recorded
|
|
428
|
-
// for the phase-end score either way). Revealing the answer + explanation on
|
|
429
|
-
// a wrong y/n is a PREMIUM learn-token feature (pass premiumExplain=true).
|
|
430
|
-
// See [[project_card_feedback_e]] feedback-tiering.
|
|
431
|
-
export function renderCheckFeedback(card, userAnswer, correct, premiumExplain = false) {
|
|
432
|
-
const isZh = lang().startsWith('zh');
|
|
433
|
-
console.log();
|
|
434
|
-
if (correct) {
|
|
435
|
-
console.log(chalk.green(' ✓ ') + chalk.white(isZh ? '答对了。' : 'Correct.'));
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
console.log(chalk.yellow(' ✗ ') + chalk.white(isZh ? '记下了,继续。' : 'Noted — moving on.'));
|
|
439
|
-
if (premiumExplain) {
|
|
440
|
-
const right = card.check?.answer === 'y' ? (isZh ? '是' : 'Yes') : isZh ? '否' : 'No';
|
|
441
|
-
console.log(chalk.gray(' ' +
|
|
442
|
-
(isZh ? '正确答案: ' : 'Answer: ') +
|
|
443
|
-
right +
|
|
444
|
-
'. ' +
|
|
445
|
-
(isZh ? '回看上面那段。' : 'Re-read the paragraph above.')));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
// Worked solution (problem cards): revealed in ALL tiers after the check, both
|
|
449
|
-
// right and wrong — on a problem card the worked solution IS the lesson. _zh
|
|
450
|
-
// overlay wins when present. The card pauses after this (learn.ts) so there is
|
|
451
|
-
// time to read it. Independent of premiumExplain (that gates a one-line hint).
|
|
452
|
-
const solution = isZh ? (card._zh?.solution ?? card.solution) : card.solution;
|
|
453
|
-
if (solution && solution.length > 0) {
|
|
454
|
-
console.log();
|
|
455
|
-
console.log(chalk.cyan(' 📝 ' + (isZh ? '解答:' : 'Worked solution:')));
|
|
456
|
-
for (const para of solution) {
|
|
457
|
-
for (const ln of wrapMixed(para, W - 4)) {
|
|
458
|
-
console.log(chalk.white(' ' + ln));
|
|
459
|
-
}
|
|
460
|
-
console.log();
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
console.log();
|
|
464
|
-
}
|
|
465
|
-
// Founder sign-off shown at the BOTTOM of score screens (peak-end placement —
|
|
466
|
-
// the name lands at the satisfaction moment, like a film's closing "Directed by").
|
|
467
|
-
// Dim, recedes below the score. NO "Author" (bodies are AI-generated → dispute
|
|
468
|
-
// surface); "Architect" states the real, undisputed contribution. NO jargon.
|
|
469
|
-
// See memory project_learn_attribution_banner_plan.
|
|
470
|
-
function attributionLines(isZh) {
|
|
471
|
-
return [
|
|
472
|
-
chalk.white(` ${isZh ? '编著:Charlie Zhu' : 'Created & edited by Charlie Zhu'}`),
|
|
473
|
-
chalk.gray(` ${isZh ? 'ICOA 2026 创始人 · 课程总架构师' : 'Founder & Chief Curriculum Architect · ICOA 2026'}`),
|
|
474
|
-
];
|
|
475
|
-
}
|
|
476
|
-
// Phase-end score screen — basic-tier feedback granularity (project_card_feedback_e):
|
|
477
|
-
// shown when a forward advance crosses a module boundary. Computes the y/n score
|
|
478
|
-
// for the just-completed phase and announces the next one. Yellow milestone-style
|
|
479
|
-
// band; ASCII dashes (CJK-safe). Does NOT pause — flows into the next card.
|
|
480
|
-
export function renderPhaseComplete(curriculum, state, done, next) {
|
|
481
|
-
const isZh = lang().startsWith('zh');
|
|
482
|
-
const [lo, hi] = done.cardRange;
|
|
483
|
-
const checks = state.checkResults || {};
|
|
484
|
-
let answered = 0;
|
|
485
|
-
let correct = 0;
|
|
486
|
-
for (let n = lo; n <= hi; n++) {
|
|
487
|
-
const r = checks[String(n)];
|
|
488
|
-
if (r) {
|
|
489
|
-
answered++;
|
|
490
|
-
if (r.correct)
|
|
491
|
-
correct++;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
const dashes = '-'.repeat(48);
|
|
495
|
-
console.log();
|
|
496
|
-
for (const l of icoaBannerLines(' '))
|
|
497
|
-
console.log(l);
|
|
498
|
-
console.log();
|
|
499
|
-
console.log(chalk.bold.yellow(' ' + dashes));
|
|
500
|
-
console.log(chalk.bold.yellow(' ' + (isZh ? `阶段 ${done.number} 完成` : `Phase ${done.number} complete`)) +
|
|
501
|
-
chalk.gray(' · ') +
|
|
502
|
-
chalk.white(done.name));
|
|
503
|
-
const scoreColor = answered > 0 && correct === answered ? chalk.bold.green : chalk.bold.white;
|
|
504
|
-
console.log(chalk.gray(' ' + (isZh ? '理解检查得分: ' : 'comprehension score: ')) +
|
|
505
|
-
scoreColor(`${correct}/${answered}`) +
|
|
506
|
-
chalk.gray(answered === 0 ? (isZh ? ' (本阶段未作答 y/n)' : ' (no y/n answered)') : ''));
|
|
507
|
-
if (next) {
|
|
508
|
-
console.log(chalk.gray(' ' + (isZh ? '下一阶段: ' : 'next: ')) +
|
|
509
|
-
chalk.bold.cyan(isZh ? `阶段 ${next.number}` : `Phase ${next.number}`) +
|
|
510
|
-
chalk.gray(' · ') +
|
|
511
|
-
chalk.white(next.name));
|
|
512
|
-
}
|
|
513
|
-
console.log(chalk.bold.yellow(' ' + dashes));
|
|
514
|
-
for (const l of attributionLines(isZh))
|
|
515
|
-
console.log(l);
|
|
516
|
-
}
|
|
517
|
-
// Curriculum-end OVERALL score — shown after the last card of the last phase.
|
|
518
|
-
// Per-phase y/n breakdown + a grand total across all modules. Green CTF band.
|
|
519
|
-
export function renderCurriculumComplete(curriculum, state) {
|
|
520
|
-
const isZh = lang().startsWith('zh');
|
|
521
|
-
const checks = state.checkResults || {};
|
|
522
|
-
const dashes = '-'.repeat(48);
|
|
523
|
-
let totA = 0;
|
|
524
|
-
let totC = 0;
|
|
525
|
-
console.log();
|
|
526
|
-
for (const l of icoaBannerLines(' '))
|
|
527
|
-
console.log(l);
|
|
528
|
-
console.log();
|
|
529
|
-
console.log(chalk.bold.green(' ' + dashes));
|
|
530
|
-
console.log(chalk.bold.green(' 🏆 ' + (isZh ? `${curriculum.name} — 全部完成!` : `${curriculum.name} — complete!`)));
|
|
531
|
-
console.log(chalk.bold.green(' ' + dashes));
|
|
532
|
-
for (const m of curriculum.modules) {
|
|
533
|
-
const [lo, hi] = m.cardRange;
|
|
534
|
-
let a = 0;
|
|
535
|
-
let c = 0;
|
|
536
|
-
for (let n = lo; n <= hi; n++) {
|
|
537
|
-
const r = checks[String(n)];
|
|
538
|
-
if (r) {
|
|
539
|
-
a++;
|
|
540
|
-
if (r.correct)
|
|
541
|
-
c++;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
totA += a;
|
|
545
|
-
totC += c;
|
|
546
|
-
const col = a > 0 && c === a ? chalk.green : chalk.white;
|
|
547
|
-
console.log(chalk.gray(' ' + (isZh ? `阶段 ${m.number}` : `Phase ${m.number}`).padEnd(8)) +
|
|
548
|
-
col(`${c}/${a}`.padEnd(7)) +
|
|
549
|
-
chalk.gray(m.name));
|
|
550
|
-
}
|
|
551
|
-
console.log(chalk.bold.green(' ' + dashes));
|
|
552
|
-
console.log(chalk.bold.green(' ' + (isZh ? '总分 / overall: ' : 'Overall: ')) + chalk.bold.white(`${totC}/${totA}`));
|
|
553
|
-
console.log(chalk.bold.green(' ' + dashes));
|
|
554
|
-
for (const l of attributionLines(isZh))
|
|
555
|
-
console.log(l);
|
|
556
|
-
console.log();
|
|
557
|
-
}
|
|
558
|
-
export function renderMCQCard(card, curriculum) {
|
|
559
|
-
const L = lang();
|
|
560
|
-
const c = localized(card, L);
|
|
561
|
-
cardHeader(c, curriculum);
|
|
562
|
-
emptyLine();
|
|
563
|
-
bodyLine(chalk.bold.yellow('🎯 ' + c.title));
|
|
564
|
-
bodyLine(chalk.gray('─'.repeat(Math.min(c.title.length + 4, W))));
|
|
565
|
-
emptyLine();
|
|
566
|
-
for (const ln of wrapText(c.question, W - 4))
|
|
567
|
-
bodyLine(' ' + ln);
|
|
568
|
-
emptyLine();
|
|
569
|
-
for (const letter of ['A', 'B', 'C', 'D']) {
|
|
570
|
-
bodyLine(chalk.cyan(` ${letter}.`) + ' ' + chalk.white(c.options[letter]));
|
|
571
|
-
}
|
|
572
|
-
emptyLine();
|
|
573
|
-
cardFooter();
|
|
574
|
-
console.log(chalk.gray(' ' + (L.startsWith('zh') ? '输入 ' : 'Type ')) +
|
|
575
|
-
chalk.bold.green('A') +
|
|
576
|
-
chalk.gray(' / ') +
|
|
577
|
-
chalk.bold.green('B') +
|
|
578
|
-
chalk.gray(' / ') +
|
|
579
|
-
chalk.bold.green('C') +
|
|
580
|
-
chalk.gray(' / ') +
|
|
581
|
-
chalk.bold.green('D') +
|
|
582
|
-
chalk.gray(' ' + t('type_to_answer', L)));
|
|
583
|
-
console.log();
|
|
584
|
-
}
|
|
585
|
-
export function renderMCQFeedback(card, selected, correct, state) {
|
|
586
|
-
const L = lang();
|
|
587
|
-
const c = localized(card, L);
|
|
588
|
-
console.log();
|
|
589
|
-
if (correct) {
|
|
590
|
-
console.log(' ' +
|
|
591
|
-
chalk.bold.green(t('correct', L) + ' ') +
|
|
592
|
-
chalk.gray(`${t('one_point', L)} · ${selected} = ${c.options[selected]}`));
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
console.log(' ' +
|
|
596
|
-
chalk.bold.red(t('not_quite', L) + ' ') +
|
|
597
|
-
chalk.gray(`${t('you_chose', L)} ${selected}; ${t('answer_is', L)} ${c.answer}.`));
|
|
598
|
-
}
|
|
599
|
-
console.log();
|
|
600
|
-
console.log(chalk.gray(' ' + t('explanation', L)));
|
|
601
|
-
for (const ln of wrapText(c.explanation, W - 4)) {
|
|
602
|
-
console.log(chalk.gray(' ' + ln));
|
|
603
|
-
}
|
|
604
|
-
const mcqEntries = Object.values(state.mcqResults);
|
|
605
|
-
const correctCount = mcqEntries.filter((e) => e.correct).length;
|
|
606
|
-
console.log();
|
|
607
|
-
console.log(chalk.gray(' ' + t('mcq_accuracy_so_far', L) + ' ') + chalk.white(`${correctCount}/${mcqEntries.length}`));
|
|
608
|
-
console.log();
|
|
609
|
-
// Correct answers auto-advance (caller bumps the card); only prompt for a
|
|
610
|
-
// manual continue when wrong, so the student can re-read the explanation.
|
|
611
|
-
if (!correct) {
|
|
612
|
-
console.log(chalk.gray(' ' + t('press_ok_continue', L) + ' ') +
|
|
613
|
-
chalk.bold.green('ok') +
|
|
614
|
-
chalk.gray(' ' + t('to_continue', L)));
|
|
615
|
-
console.log();
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
export function renderPracticalCard(card, curriculum) {
|
|
619
|
-
const L = lang();
|
|
620
|
-
const c = localized(card, L);
|
|
621
|
-
cardHeader(c, curriculum);
|
|
622
|
-
emptyLine();
|
|
623
|
-
bodyLine(chalk.bold.yellow('🛠 ' + c.title));
|
|
624
|
-
bodyLine(chalk.gray('─'.repeat(Math.min(c.title.length + 4, W))));
|
|
625
|
-
emptyLine();
|
|
626
|
-
for (const ln of wrapText(c.task, W - 4))
|
|
627
|
-
bodyLine(' ' + ln);
|
|
628
|
-
if (c.starterCode) {
|
|
629
|
-
emptyLine();
|
|
630
|
-
bodyLine(chalk.gray(' ' + t('starter_code', L)));
|
|
631
|
-
emptyLine();
|
|
632
|
-
for (const ln of c.starterCode.split('\n')) {
|
|
633
|
-
bodyLine(' ' + chalk.cyan(ln));
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
emptyLine();
|
|
637
|
-
cardFooter();
|
|
638
|
-
console.log(chalk.gray(' ' + t('try_in_sandbox', L) + ' ') +
|
|
639
|
-
chalk.bold.cyan('!python3') +
|
|
640
|
-
chalk.gray(' ' + t('drops_into_python', L)));
|
|
641
|
-
console.log(chalk.gray(' ' + t('when_done', L)));
|
|
642
|
-
console.log(chalk.gray(' ') + chalk.bold.green('done') + chalk.gray(' ' + t('done_desc', L)));
|
|
643
|
-
console.log(chalk.gray(' ') + chalk.bold.yellow('skip') + chalk.gray(' ' + t('skip_desc', L)));
|
|
644
|
-
console.log(chalk.gray(' card N jump to a card by number'));
|
|
645
|
-
console.log();
|
|
646
|
-
}
|
|
647
|
-
export function renderPracticalSuccess(card) {
|
|
648
|
-
const L = lang();
|
|
649
|
-
const c = localized(card, L);
|
|
650
|
-
console.log();
|
|
651
|
-
console.log(' ' + chalk.bold.green(t('practical_recorded', L)));
|
|
652
|
-
console.log();
|
|
653
|
-
console.log(chalk.gray(' ' + t('reference_answer', L)));
|
|
654
|
-
for (const ln of wrapText(c.successHint, W - 4)) {
|
|
655
|
-
console.log(chalk.gray(' ' + ln));
|
|
656
|
-
}
|
|
657
|
-
console.log();
|
|
658
|
-
console.log(chalk.gray(' ' + t('press_ok_continue', L) + ' ') + chalk.bold.green('ok') + chalk.gray(' ' + t('to_continue', L)));
|
|
659
|
-
console.log();
|
|
660
|
-
}
|
|
661
|
-
export function renderSimDemoCard(card, curriculum) {
|
|
662
|
-
const L = lang();
|
|
663
|
-
const c = localized(card, L);
|
|
664
|
-
cardHeader(c, curriculum);
|
|
665
|
-
emptyLine();
|
|
666
|
-
bodyLine(chalk.bold.yellow('🎬 ' + c.title));
|
|
667
|
-
emptyLine();
|
|
668
|
-
for (const ln of wrapText(c.description, W - 4))
|
|
669
|
-
bodyLine(' ' + ln);
|
|
670
|
-
emptyLine();
|
|
671
|
-
bodyLine(chalk.gray(' ' + t('sim_requires', L)));
|
|
672
|
-
emptyLine();
|
|
673
|
-
cardFooter();
|
|
674
|
-
// Action prompts — same style as knowledge cards so students always
|
|
675
|
-
// know how to advance. Without these the sim_demo is a dead-end.
|
|
676
|
-
console.log(chalk.gray(' ') + chalk.bold.cyan('sim') + chalk.gray(' ' + t('sim_launch_desc', L)));
|
|
677
|
-
console.log(chalk.gray(' ') +
|
|
678
|
-
chalk.bold.green('ok') +
|
|
679
|
-
chalk.gray(' / ') +
|
|
680
|
-
chalk.bold.green('next') +
|
|
681
|
-
chalk.gray(' ' + t('continue_to_next', L)));
|
|
682
|
-
console.log(chalk.gray(' bookmark ' + t('bookmark_desc', L)));
|
|
683
|
-
console.log(chalk.gray(' back ' + t('back_desc', L)));
|
|
684
|
-
console.log(chalk.gray(' card N jump to a card by number'));
|
|
685
|
-
console.log(chalk.gray(' quit ' + t('quit_desc', L)));
|
|
686
|
-
console.log();
|
|
687
|
-
}
|
|
688
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
689
|
-
// Arena task (<track>-frontier) — the READING surface. Hands-on doing lives
|
|
690
|
-
// in the standalone `icoa arena [track] [task]` command (mirrors sim_demo → sim).
|
|
691
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
692
|
-
export function renderArenaTaskCard(card, curriculum) {
|
|
693
|
-
const L = lang();
|
|
694
|
-
const isZh = L.startsWith('zh');
|
|
695
|
-
const c = localized(card, L);
|
|
696
|
-
cardHeader(c, curriculum);
|
|
697
|
-
emptyLine();
|
|
698
|
-
bodyLine(chalk.bold.cyan('🏟 ' + c.title));
|
|
699
|
-
bodyLine(chalk.gray(' ' + c.origin + ' · ' + c.domain + ' · ' + c.metric));
|
|
700
|
-
emptyLine();
|
|
701
|
-
for (const para of c.statement) {
|
|
702
|
-
for (const ln of wrapText(para, W - 4))
|
|
703
|
-
bodyLine(' ' + ln);
|
|
704
|
-
emptyLine();
|
|
705
|
-
}
|
|
706
|
-
bodyLine(chalk.bold.green(isZh ? ' 核心要点 — baseline → 解法的关键变化' : ' CORE DELTA — baseline → solution'));
|
|
707
|
-
for (const d of c.core_delta) {
|
|
708
|
-
for (const ln of wrapText('• ' + d, W - 6))
|
|
709
|
-
bodyLine(' ' + chalk.white(ln));
|
|
710
|
-
}
|
|
711
|
-
emptyLine();
|
|
712
|
-
bodyLine(chalk.gray((isZh ? ' 提交格式: ' : ' Submission: ') + c.submission_format));
|
|
713
|
-
if (c.hints) {
|
|
714
|
-
bodyLine(chalk.gray(isZh
|
|
715
|
-
? ' 本题有 5 级提示 (a–e),进入 arena 后逐级解锁。'
|
|
716
|
-
: ' 5 progressive hints (a–e) unlock inside the arena.'));
|
|
717
|
-
}
|
|
718
|
-
emptyLine();
|
|
719
|
-
cardFooter();
|
|
720
|
-
console.log(chalk.gray(' ') +
|
|
721
|
-
chalk.bold.cyan('arena ' + c.task_id) +
|
|
722
|
-
chalk.gray(isZh ? ' 动手做这道题(下载 / 提示 / 提交 / 榜单)' : ' open the arena (download / hints / submit / board)'));
|
|
723
|
-
console.log(chalk.gray(' ') +
|
|
724
|
-
chalk.bold.green('ok') +
|
|
725
|
-
chalk.gray(' / ') +
|
|
726
|
-
chalk.bold.green('next') +
|
|
727
|
-
chalk.gray(isZh ? ' 继续下一张' : ' continue to next'));
|
|
728
|
-
console.log(chalk.gray(' back ' + (isZh ? '上一张' : 'previous')));
|
|
729
|
-
console.log(chalk.gray(' card N jump to a card by number'));
|
|
730
|
-
console.log(chalk.gray(' quit ' + (isZh ? '退出' : 'exit')));
|
|
731
|
-
console.log();
|
|
732
|
-
}
|
|
733
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
734
|
-
// Coming-soon placeholder (frontier-120 partial publish)
|
|
735
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
736
|
-
export function renderComingSoonCard(card, curriculum) {
|
|
737
|
-
const L = lang();
|
|
738
|
-
const isZh = L.startsWith('zh');
|
|
739
|
-
const c = localized(card, L);
|
|
740
|
-
cardHeader(c, curriculum);
|
|
741
|
-
emptyLine();
|
|
742
|
-
bodyLine(chalk.bold.yellow('🔜 ' + c.title));
|
|
743
|
-
emptyLine();
|
|
744
|
-
const note = c.note || (isZh ? '这道竞技场题目正在编写中,敬请期待。' : 'This arena task is coming soon.');
|
|
745
|
-
for (const ln of wrapText(note, W - 4))
|
|
746
|
-
bodyLine(' ' + chalk.gray(ln));
|
|
747
|
-
emptyLine();
|
|
748
|
-
cardFooter();
|
|
749
|
-
console.log(chalk.gray(' ') +
|
|
750
|
-
chalk.bold.green('ok') +
|
|
751
|
-
chalk.gray(' / ') +
|
|
752
|
-
chalk.bold.green('next') +
|
|
753
|
-
chalk.gray(isZh ? ' 继续' : ' continue'));
|
|
754
|
-
console.log(chalk.gray(' card N jump to a card by number'));
|
|
755
|
-
console.log(chalk.gray(' quit ' + (isZh ? '退出' : 'exit')));
|
|
756
|
-
console.log();
|
|
757
|
-
}
|
|
758
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
759
|
-
// Milestone — the big celebration
|
|
760
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
761
|
-
export function renderMilestone(card, curriculum) {
|
|
762
|
-
const L = lang();
|
|
763
|
-
const c = localized(card, L);
|
|
764
|
-
const isFinal = card.number === curriculum.totalCards;
|
|
765
|
-
console.log();
|
|
766
|
-
console.log(chalk.bold.yellow(' ╭' + rule('═')));
|
|
767
|
-
console.log(chalk.bold.yellow(' ║'));
|
|
768
|
-
console.log(chalk.bold.yellow(' ║ ') + chalk.white(' ' + t('milestone_header', L)));
|
|
769
|
-
console.log(chalk.bold.yellow(' ║'));
|
|
770
|
-
const badgeLine = ` ${c.badge} ${c.emoji}`;
|
|
771
|
-
console.log(chalk.bold.yellow(' ║ ') + chalk.bold.green(badgeLine));
|
|
772
|
-
console.log(chalk.bold.yellow(' ║ ') + chalk.gray(' ─'.repeat(Math.max(1, Math.floor(c.badge.length / 2 + 2)))));
|
|
773
|
-
console.log(chalk.bold.yellow(' ║'));
|
|
774
|
-
console.log(chalk.bold.yellow(' ║ ') + ' ' + chalk.gray(t('in_wild_corresponds', L)));
|
|
775
|
-
for (const ln of wrapText(c.realWorldLevel, W - 4)) {
|
|
776
|
-
console.log(chalk.bold.yellow(' ║ ') + ' ' + chalk.white(ln));
|
|
777
|
-
}
|
|
778
|
-
console.log(chalk.bold.yellow(' ║'));
|
|
779
|
-
console.log(chalk.bold.yellow(' ║ ') + ' ' + chalk.gray(t('whats_next', L)));
|
|
780
|
-
for (const ln of wrapText(c.unlockedNext, W - 4)) {
|
|
781
|
-
console.log(chalk.bold.yellow(' ║ ') + ' ' + chalk.white(ln));
|
|
782
|
-
}
|
|
783
|
-
console.log(chalk.bold.yellow(' ║'));
|
|
784
|
-
console.log(chalk.bold.yellow(' ╰' + rule('═')));
|
|
785
|
-
console.log();
|
|
786
|
-
if (isFinal) {
|
|
787
|
-
// Curriculum-end ceremony — what LEARNDEMO01 + n=100/480 ends with.
|
|
788
|
-
console.log(chalk.gray(' ' + t('demo_complete', L)));
|
|
789
|
-
console.log();
|
|
790
|
-
console.log(chalk.gray(' ' + t('unlock_full', L) + ' ') + chalk.white(t('curriculum_name', L)) + chalk.gray(','));
|
|
791
|
-
console.log(chalk.gray(' ' + t('contact_team_leader', L) + ' ') +
|
|
792
|
-
chalk.bold.yellow('EA') +
|
|
793
|
-
chalk.gray(' ' + t('learn_token', L)));
|
|
794
|
-
console.log(chalk.gray(' ' + t('or_email', L) + ' ') +
|
|
795
|
-
chalk.cyan('australia@icoa2026.au') +
|
|
796
|
-
chalk.gray(' ' + t('for_partnership', L)));
|
|
797
|
-
console.log();
|
|
798
|
-
console.log(chalk.gray(' ' + t('type_quit', L) + ' ') +
|
|
799
|
-
chalk.bold.green('quit') +
|
|
800
|
-
chalk.gray(' ' + t('to_exit', L) + ' ') +
|
|
801
|
-
chalk.bold.green('status') +
|
|
802
|
-
chalk.gray(' ' + t('for_dashboard', L)));
|
|
803
|
-
}
|
|
804
|
-
else {
|
|
805
|
-
// Mid-curriculum phase-end — must show advance prompt, not "quit".
|
|
806
|
-
console.log(chalk.gray(' ') +
|
|
807
|
-
chalk.bold.green('ok') +
|
|
808
|
-
chalk.gray(' / ') +
|
|
809
|
-
chalk.bold.green('next') +
|
|
810
|
-
chalk.gray(' ' + t('continue_to_next', L)));
|
|
811
|
-
console.log(chalk.gray(' status ' + t('status_full', L)));
|
|
812
|
-
console.log(chalk.gray(' card N jump to a card by number'));
|
|
813
|
-
console.log(chalk.gray(' quit ' + t('quit_desc', L)));
|
|
814
|
-
}
|
|
815
|
-
console.log();
|
|
816
|
-
}
|
|
817
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
818
|
-
// Status dashboard
|
|
819
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
820
|
-
export function renderStatus(curriculum, state) {
|
|
821
|
-
const L = lang();
|
|
822
|
-
const done = state.cardsCompleted.length;
|
|
823
|
-
const total = curriculum.totalCards;
|
|
824
|
-
const mcqEntries = Object.values(state.mcqResults);
|
|
825
|
-
const mcqCorrect = mcqEntries.filter((e) => e.correct).length;
|
|
826
|
-
console.log();
|
|
827
|
-
console.log(chalk.cyan(' ╭─ ') + chalk.bold.white(t('status_title', L)) + ' ' + chalk.cyan('─'.repeat(25)));
|
|
828
|
-
emptyLine();
|
|
829
|
-
bodyLine(' ' + chalk.gray(t('total_progress', L) + ' ') + progressBar(done, total) + chalk.gray(` (${done}/${total})`));
|
|
830
|
-
bodyLine(' ' +
|
|
831
|
-
chalk.gray(t('streak', L) + ' ') +
|
|
832
|
-
fireStreak(state.streakDays) +
|
|
833
|
-
chalk.gray(` (${t('longest', L)} ${state.longestStreak})`));
|
|
834
|
-
bodyLine(' ' + chalk.gray(t('mcq_accuracy', L) + ' ') + chalk.white(`${mcqCorrect}/${mcqEntries.length}`));
|
|
835
|
-
bodyLine(' ' + chalk.gray(t('practicals_done', L) + ' ') + chalk.white(`${state.practicalsCompleted.length}`));
|
|
836
|
-
bodyLine(' ' + chalk.gray(t('bookmarked', L) + ' ') + chalk.white(`${state.bookmarks.length}`));
|
|
837
|
-
emptyLine();
|
|
838
|
-
const moduleLabel = t('module', L).replace(':', '').trim();
|
|
839
|
-
for (const m of curriculum.modules) {
|
|
840
|
-
const [lo, hi] = m.cardRange;
|
|
841
|
-
const inRange = state.cardsCompleted.filter((c) => c >= lo && c <= hi).length;
|
|
842
|
-
const moduleTotal = hi - lo + 1;
|
|
843
|
-
const status = inRange === moduleTotal ? chalk.green('✓') : inRange > 0 ? chalk.yellow('▶') : chalk.gray('□');
|
|
844
|
-
bodyLine(' ' +
|
|
845
|
-
status +
|
|
846
|
-
' ' +
|
|
847
|
-
chalk.gray(`${moduleLabel} ${m.number}: `) +
|
|
848
|
-
chalk.white(`${inRange}/${moduleTotal}`) +
|
|
849
|
-
chalk.gray(' ' + m.name));
|
|
850
|
-
}
|
|
851
|
-
emptyLine();
|
|
852
|
-
if (state.achievements.length > 0) {
|
|
853
|
-
bodyLine(' ' + chalk.gray(t('achievements', L)));
|
|
854
|
-
for (const a of state.achievements) {
|
|
855
|
-
bodyLine(' ' + chalk.bold.yellow('★ ') + chalk.white(a));
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
else {
|
|
859
|
-
bodyLine(' ' + chalk.gray(t('achievements_none', L)));
|
|
860
|
-
}
|
|
861
|
-
emptyLine();
|
|
862
|
-
cardFooter();
|
|
863
|
-
}
|
|
1
|
+
import chalk from"chalk";import o from"string-width";import{localized as e}from"./learn-curricula.js";import{t as n}from"./learn-i18n.js";import{getConfig as l}from"./config.js";import{icoaBannerLines as t}from"./banner.js";import{renderCharts as r,detectCaps as c}from"./render-card.js";function s(){return(l().language||"en").toLowerCase()}const a=64;function g(o="─"){return o.repeat(66)}function i(o,e,n=20){const l=e>0?o/e:0,t=Math.floor(l*n),r=n-t;return`${chalk.green("█".repeat(t))+chalk.gray("░".repeat(r))} ${(100*l).toFixed(1)}%`}function d(o){return o>=30?chalk.red("🔥🔥🔥 "+o+" days"):o>=7?chalk.yellow("🔥 "+o+" days"):o>=1?chalk.gray("· "+o+" day"+(o>1?"s":"")):chalk.gray("—")}export function renderWelcome(o,e,l){const t=s(),r=e.cardsCompleted.length,c=o.totalCards,a=c-r,y=o.modules.find(o=>e.currentCard>=o.cardRange[0]&&e.currentCard<=o.cardRange[1]),u=/demo/i.test(o.id);if(console.log(),console.log(chalk.cyan(" ╭"+g("═"))),console.log(chalk.cyan(" ║")),console.log(chalk.cyan(" ║ ")+chalk.bold.white(" "+(o.name?.trim()||n("academy_title",t)))),console.log(chalk.cyan(" ║")),l)console.log(chalk.cyan(" ║ ")+chalk.white(" "+n(u?"welcome_new_demo":"welcome_new",t)));else{const o=new Date(e.lastSeenAt),l=Math.floor((Date.now()-o.getTime())/36e5),r=l<1?n("just_now",t):l<24?`${l}${n("ago_hours",t)}`:`${Math.floor(l/24)}${n("ago_days",t)}`;console.log(chalk.cyan(" ║ ")+chalk.white(` ${n("welcome_back",t)} ${r}`))}console.log(chalk.cyan(" ║"));const h=y?`${y.number}. ${y.name}`:"—";console.log(chalk.cyan(" ║ ")+" "+chalk.gray(n("module",t).padEnd(11))+chalk.white(h)),console.log(chalk.cyan(" ║ ")+" "+chalk.gray(n("progress",t).padEnd(11))+i(r,c)+chalk.gray(` (${r}/${c})`)),console.log(chalk.cyan(" ║ ")+" "+chalk.gray(n("streak",t).padEnd(11))+d(e.streakDays)),a>0&&e.currentCard<=c&&console.log(chalk.cyan(" ║ ")+" "+chalk.gray(n("next_card",t).padEnd(11))+chalk.white(`#${e.currentCard} ${n("of",t)} ${c}`)),console.log(chalk.cyan(" ║")),console.log(chalk.cyan(" ╰"+g("═"))),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────"));const m=0===r?n("continue_start",t):`${n("continue_resume",t)} ${e.currentCard}`;console.log(chalk.bold.green(" continue")+chalk.gray(" "+m)),console.log(chalk.yellow(" status")+chalk.gray(" "+n("status_full",t))),e.bookmarks.length>0&&console.log(chalk.yellow(" bookmarks")+chalk.gray(` ${e.bookmarks.length} ${n("bookmarks_desc",t)}`)),console.log(chalk.gray(" quit")+chalk.gray(" "+n("quit_desc",t))),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log()}function y(o,e){const l=s(),t=o.module,r=e.modules.find(o=>o.number===t),c=r?r.name:n("unknown_module",l),g=n("module",l).replace(":","").trim(),i=`${n("card",l)} ${o.number} / ${e.totalCards} · ${g} ${t} · ${c}`;console.log(),console.log(chalk.cyan(" ╭─ ")+chalk.bold.white(i)+" "+chalk.cyan("─".repeat(Math.max(0,a-i.length-4))))}function u(){console.log(chalk.cyan(" ╰"+g())),console.log()}function h(o){console.log(chalk.cyan(" │ ")+chalk.white(o))}function m(){console.log(chalk.cyan(" │"))}const f=/[┌┐└┘├┤┬┴┼─│╔╗╚╝═║╠╣╦╩╬▓▒░█]/;function b(o){return/^(?: {4}|\t)/.test(o)||f.test(o)||/ {3,}/.test(o)}function p(e,n){const l=[];let t="",r=0,c=!1;for(const s of function(e){const n=[],l=e=>{let l="";const t=()=>{l&&(n.push({t:"atom",s:l}),l="")};for(const r of e)" "===r?(t(),n.push({t:"space"})):2===o(r)?(t(),n.push({t:"wide",s:r})):l+=r;t()},t=/`[^`]*`/g;let r,c=0;for(;null!==(r=t.exec(e));)l(e.slice(c,r.index)),n.push({t:"code",s:r[0]}),c=r.index+r[0].length;return l(e.slice(c)),n}(e)){if("space"===s.t){""!==t&&(c=!0);continue}const e=s.s,a=o("code"===s.t?e.replace(/`/g,""):e),g=c?1:0,i="atom"===s.t&&e.length<=2&&/^[.,;:!?)\]}…'"]+$/.test(e);""!==t&&!i&&r+g+a>n?(l.push(t),t=e,r=a,c=!1):(c&&(t+=" ",r+=1,c=!1),t+=e,r+=a)}return t&&l.push(t),l}const w=/\b(?:Common bug|Common mistake|Common error|Pitfall|Gotcha|Footgun)\b[::]?|\b(?:Warning|Caution)\b[::]|(?:常见错误|常见陷阱|常见误区|陷阱|注意|警告|误区)[::]/gi;function _(o,e){const n=e?chalk.bold.white:chalk.white;let l,t="",r=0;for(w.lastIndex=0;null!==(l=w.exec(o));)l.index>r&&(t+=n(o.slice(r,l.index))),t+=chalk.bold.yellow(l[0]),r=l.index+l[0].length;return r<o.length&&(t+=n(o.slice(r))),t}function $(o,e=!1){let n="",l=0;const t=/`([^`]*)`/g;let r;for(;null!==(r=t.exec(o));)r.index>l&&(n+=_(o.slice(l,r.index),e)),n+=chalk.cyan(r[1]),l=r.index+r[0].length;return l<o.length&&(n+=_(o.slice(l),e)),n}function k(o,e=!1){let n=!1;for(const l of o.split("\n"))if(/^\s*```/.test(l))n=!n;else if(n)console.log(chalk.cyan(" │ ")+chalk.cyan(` ${l}`));else if(""!==l)if(b(l))console.log(chalk.cyan(" │ ")+chalk.white(l));else for(const o of p(l,60))console.log(chalk.cyan(" │ ")+` ${$(o,e)}`);else m()}function C(o,e){const n=[];for(const l of o.split("\n")){if(""===l){n.push("");continue}if(b(l)){n.push(l);continue}let o="";for(const t of l.split(" "))(o+" "+t).trim().length>e?(n.push(o.trim()),o=t):o=(o+" "+t).trim();o&&n.push(o)}return n}export function renderKnowledgeCard(l,t){const g=s(),i=e(l,g);y(i,t),m();const d=p(i.title,60);for(const o of d)h(chalk.bold.yellow(o));h(chalk.gray("─".repeat(Math.min(Math.max(...d.map(e=>o(e))),a))));const f=!!(l.chart_ids&&l.chart_ids.length>0);if(f&&r(l.chart_ids.slice(0,1),t.charts,c()),i.key_point&&""!==i.key_point.trim()?(f||m(),function(o,e,n){const l=s().startsWith("zh"),t=c().cols,r="-".repeat(Math.min(a,Math.max(24,t-6))),g=l?"要点":"KEY POINT",i=l?`第 ${e} / ${n} 张`:`Card ${e} / ${n}`;console.log(chalk.cyan(" │ ")+chalk.green(r)),console.log(chalk.cyan(" │ ")+chalk.green(`${g} · ${i}`));for(const e of p(o,60))console.log(chalk.cyan(" │ ")+chalk.bold.green(" "+e));console.log(chalk.cyan(" │ ")+chalk.green(r)),m()}(i.key_point,i.number,t.totalCards)):f||m(),i.body.filter(o=>null!=o&&""!==o.trim()).forEach((o,e)=>{const n=0===e?o.match(/^([\s\S]*?(?:[.!?](?=\s|$)|[。!?]))(\s[\s\S]*)?$/):null;if(n&&n[1].trim().length<=180){k(n[1],!0);const o=n[2]?.trimStart();o&&k(o,!1)}else k(o,!1);m()}),i.icoaConnection){h(chalk.magenta(" "+n("icoa_connection",g))),h(chalk.gray(" "+"─".repeat(20)));for(const o of p(i.icoaConnection,60))h(" "+chalk.magenta(o));m()}if(l.check){const o=g.startsWith("zh"),e=l._zh?.checkStatement??l._zh?.check?.statement,n=o?e??l.check.statement:l.check.statement;h(chalk.cyan(" 🤔 "+(o?"请回答 (输 y 或 n):":"Quick comprehension check (type y or n):"))),h(chalk.gray(" "+"─".repeat(20)));for(const o of p(n,60))h(" "+chalk.white(o));m()}u(),function(o,e){const n=s().startsWith("zh"),l=c().cols,t="-".repeat(Math.min(a,Math.max(24,l-6))),r=chalk.gray(" "),g=[];o.check?(g.push(chalk.bold.green("[y]")+chalk.gray(n?" 是":" yes")),g.push(chalk.bold.green("[n]")+chalk.gray(n?" 否":" no"))):g.push(chalk.bold.green("[ok]")+chalk.gray(n?" 继续":" next")),g.push(chalk.bold.yellow("[e]")+chalk.gray(n?" 报告问题":" report")),g.push(chalk.bold.cyan("[b]")+chalk.gray(n?" 上一张":" back")),g.push(chalk.bold.cyan("[card N]")+chalk.gray(n?" 跳转":" jump")),g.push(chalk.gray("[quit]")+chalk.gray(n?" 退出":" quit")),console.log(chalk.gray(" "+t)),console.log(chalk.gray(" "+(n?`第 ${o.number} / ${e.totalCards} 张`:`Card ${o.number} / ${e.totalCards}`))),console.log(" "+g.join(r)),console.log(chalk.gray(" "+t)),console.log(chalk.gray(" "+(n?"e = 报告本卡错误,不算作答;提交后回到本题继续":"e = report a problem with this card (not an answer); you return here after"))),console.log(chalk.gray(" "+(n?"更多: bookmark · status · lang zh/en":"more: bookmark · status · lang zh/en"))),console.log()}(l,t)}export function renderCheckFeedback(o,e,n,l=!1){const t=s().startsWith("zh");if(console.log(),n)console.log(chalk.green(" ✓ ")+chalk.white(t?"答对了。":"Correct."));else if(console.log(chalk.yellow(" ✗ ")+chalk.white(t?"记下了,继续。":"Noted — moving on.")),l){const e="y"===o.check?.answer?t?"是":"Yes":t?"否":"No";console.log(chalk.gray(" "+(t?"正确答案: ":"Answer: ")+e+". "+(t?"回看上面那段。":"Re-read the paragraph above.")))}const r=t?o._zh?.solution??o.solution:o.solution;if(r&&r.length>0){console.log(),console.log(chalk.cyan(" 📝 "+(t?"解答:":"Worked solution:")));for(const o of r){for(const e of p(o,60))console.log(chalk.white(" "+e));console.log()}}console.log()}function x(o){return[chalk.white(" "+(o?"编著:Charlie Zhu":"Created & edited by Charlie Zhu")),chalk.gray(" "+(o?"ICOA 2026 创始人 · 课程总架构师":"Founder & Chief Curriculum Architect · ICOA 2026"))]}export function renderPhaseComplete(o,e,n,l){const r=s().startsWith("zh"),[c,a]=n.cardRange,g=e.checkResults||{};let i=0,d=0;for(let o=c;o<=a;o++){const e=g[String(o)];e&&(i++,e.correct&&d++)}const y="-".repeat(48);console.log();for(const o of t(" "))console.log(o);console.log(),console.log(chalk.bold.yellow(" "+y)),console.log(chalk.bold.yellow(" "+(r?`阶段 ${n.number} 完成`:`Phase ${n.number} complete`))+chalk.gray(" · ")+chalk.white(n.name));const u=i>0&&d===i?chalk.bold.green:chalk.bold.white;console.log(chalk.gray(" "+(r?"理解检查得分: ":"comprehension score: "))+u(`${d}/${i}`)+chalk.gray(0===i?r?" (本阶段未作答 y/n)":" (no y/n answered)":"")),l&&console.log(chalk.gray(" "+(r?"下一阶段: ":"next: "))+chalk.bold.cyan(r?`阶段 ${l.number}`:`Phase ${l.number}`)+chalk.gray(" · ")+chalk.white(l.name)),console.log(chalk.bold.yellow(" "+y));for(const o of x(r))console.log(o)}export function renderCurriculumComplete(o,e){const n=s().startsWith("zh"),l=e.checkResults||{},r="-".repeat(48);let c=0,a=0;console.log();for(const o of t(" "))console.log(o);console.log(),console.log(chalk.bold.green(" "+r)),console.log(chalk.bold.green(" 🏆 "+(n?`${o.name} — 全部完成!`:`${o.name} — complete!`))),console.log(chalk.bold.green(" "+r));for(const e of o.modules){const[o,t]=e.cardRange;let r=0,s=0;for(let e=o;e<=t;e++){const o=l[String(e)];o&&(r++,o.correct&&s++)}c+=r,a+=s;const g=r>0&&s===r?chalk.green:chalk.white;console.log(chalk.gray(" "+(n?`阶段 ${e.number}`:`Phase ${e.number}`).padEnd(8))+g(`${s}/${r}`.padEnd(7))+chalk.gray(e.name))}console.log(chalk.bold.green(" "+r)),console.log(chalk.bold.green(" "+(n?"总分 / overall: ":"Overall: "))+chalk.bold.white(`${a}/${c}`)),console.log(chalk.bold.green(" "+r));for(const o of x(n))console.log(o);console.log()}export function renderMCQCard(o,l){const t=s(),r=e(o,t);y(r,l),m(),h(chalk.bold.yellow("🎯 "+r.title)),h(chalk.gray("─".repeat(Math.min(r.title.length+4,a)))),m();for(const o of C(r.question,60))h(" "+o);m();for(const o of["A","B","C","D"])h(chalk.cyan(` ${o}.`)+" "+chalk.white(r.options[o]));m(),u(),console.log(chalk.gray(" "+(t.startsWith("zh")?"输入 ":"Type "))+chalk.bold.green("A")+chalk.gray(" / ")+chalk.bold.green("B")+chalk.gray(" / ")+chalk.bold.green("C")+chalk.gray(" / ")+chalk.bold.green("D")+chalk.gray(" "+n("type_to_answer",t))),console.log()}export function renderMCQFeedback(o,l,t,r){const c=s(),a=e(o,c);console.log(),t?console.log(" "+chalk.bold.green(n("correct",c)+" ")+chalk.gray(`${n("one_point",c)} · ${l} = ${a.options[l]}`)):console.log(" "+chalk.bold.red(n("not_quite",c)+" ")+chalk.gray(`${n("you_chose",c)} ${l}; ${n("answer_is",c)} ${a.answer}.`)),console.log(),console.log(chalk.gray(" "+n("explanation",c)));for(const o of C(a.explanation,60))console.log(chalk.gray(" "+o));const g=Object.values(r.mcqResults),i=g.filter(o=>o.correct).length;console.log(),console.log(chalk.gray(" "+n("mcq_accuracy_so_far",c)+" ")+chalk.white(`${i}/${g.length}`)),console.log(),t||(console.log(chalk.gray(" "+n("press_ok_continue",c)+" ")+chalk.bold.green("ok")+chalk.gray(" "+n("to_continue",c))),console.log())}export function renderPracticalCard(o,l){const t=s(),r=e(o,t);y(r,l),m(),h(chalk.bold.yellow("🛠 "+r.title)),h(chalk.gray("─".repeat(Math.min(r.title.length+4,a)))),m();for(const o of C(r.task,60))h(" "+o);if(r.starterCode){m(),h(chalk.gray(" "+n("starter_code",t))),m();for(const o of r.starterCode.split("\n"))h(" "+chalk.cyan(o))}m(),u(),console.log(chalk.gray(" "+n("try_in_sandbox",t)+" ")+chalk.bold.cyan("!python3")+chalk.gray(" "+n("drops_into_python",t))),console.log(chalk.gray(" "+n("when_done",t))),console.log(chalk.gray(" ")+chalk.bold.green("done")+chalk.gray(" "+n("done_desc",t))),console.log(chalk.gray(" ")+chalk.bold.yellow("skip")+chalk.gray(" "+n("skip_desc",t))),console.log(chalk.gray(" card N jump to a card by number")),console.log()}export function renderPracticalSuccess(o){const l=s(),t=e(o,l);console.log(),console.log(" "+chalk.bold.green(n("practical_recorded",l))),console.log(),console.log(chalk.gray(" "+n("reference_answer",l)));for(const o of C(t.successHint,60))console.log(chalk.gray(" "+o));console.log(),console.log(chalk.gray(" "+n("press_ok_continue",l)+" ")+chalk.bold.green("ok")+chalk.gray(" "+n("to_continue",l))),console.log()}export function renderSimDemoCard(o,l){const t=s(),r=e(o,t);y(r,l),m(),h(chalk.bold.yellow("🎬 "+r.title)),m();for(const o of C(r.description,60))h(" "+o);m(),h(chalk.gray(" "+n("sim_requires",t))),m(),u(),console.log(chalk.gray(" ")+chalk.bold.cyan("sim")+chalk.gray(" "+n("sim_launch_desc",t))),console.log(chalk.gray(" ")+chalk.bold.green("ok")+chalk.gray(" / ")+chalk.bold.green("next")+chalk.gray(" "+n("continue_to_next",t))),console.log(chalk.gray(" bookmark "+n("bookmark_desc",t))),console.log(chalk.gray(" back "+n("back_desc",t))),console.log(chalk.gray(" card N jump to a card by number")),console.log(chalk.gray(" quit "+n("quit_desc",t))),console.log()}export function renderArenaTaskCard(o,n){const l=s(),t=l.startsWith("zh"),r=e(o,l);y(r,n),m(),h(chalk.bold.cyan("🏟 "+r.title)),h(chalk.gray(" "+r.origin+" · "+r.domain+" · "+r.metric)),m();for(const o of r.statement){for(const e of C(o,60))h(" "+e);m()}h(chalk.bold.green(t?" 核心要点 — baseline → 解法的关键变化":" CORE DELTA — baseline → solution"));for(const o of r.core_delta)for(const e of C("• "+o,58))h(" "+chalk.white(e));m(),h(chalk.gray((t?" 提交格式: ":" Submission: ")+r.submission_format)),r.hints&&h(chalk.gray(t?" 本题有 5 级提示 (a–e),进入 arena 后逐级解锁。":" 5 progressive hints (a–e) unlock inside the arena.")),m(),u(),console.log(chalk.gray(" ")+chalk.bold.cyan("arena "+r.task_id)+chalk.gray(t?" 动手做这道题(下载 / 提示 / 提交 / 榜单)":" open the arena (download / hints / submit / board)")),console.log(chalk.gray(" ")+chalk.bold.green("ok")+chalk.gray(" / ")+chalk.bold.green("next")+chalk.gray(t?" 继续下一张":" continue to next")),console.log(chalk.gray(" back "+(t?"上一张":"previous"))),console.log(chalk.gray(" card N jump to a card by number")),console.log(chalk.gray(" quit "+(t?"退出":"exit"))),console.log()}export function renderComingSoonCard(o,n){const l=s(),t=l.startsWith("zh"),r=e(o,l);y(r,n),m(),h(chalk.bold.yellow("🔜 "+r.title)),m();const c=r.note||(t?"这道竞技场题目正在编写中,敬请期待。":"This arena task is coming soon.");for(const o of C(c,60))h(" "+chalk.gray(o));m(),u(),console.log(chalk.gray(" ")+chalk.bold.green("ok")+chalk.gray(" / ")+chalk.bold.green("next")+chalk.gray(t?" 继续":" continue")),console.log(chalk.gray(" card N jump to a card by number")),console.log(chalk.gray(" quit "+(t?"退出":"exit"))),console.log()}export function renderMilestone(o,l){const t=s(),r=e(o,t),c=o.number===l.totalCards;console.log(),console.log(chalk.bold.yellow(" ╭"+g("═"))),console.log(chalk.bold.yellow(" ║")),console.log(chalk.bold.yellow(" ║ ")+chalk.white(" "+n("milestone_header",t))),console.log(chalk.bold.yellow(" ║"));const a=` ${r.badge} ${r.emoji}`;console.log(chalk.bold.yellow(" ║ ")+chalk.bold.green(a)),console.log(chalk.bold.yellow(" ║ ")+chalk.gray(" ─".repeat(Math.max(1,Math.floor(r.badge.length/2+2))))),console.log(chalk.bold.yellow(" ║")),console.log(chalk.bold.yellow(" ║ ")+" "+chalk.gray(n("in_wild_corresponds",t)));for(const o of C(r.realWorldLevel,60))console.log(chalk.bold.yellow(" ║ ")+" "+chalk.white(o));console.log(chalk.bold.yellow(" ║")),console.log(chalk.bold.yellow(" ║ ")+" "+chalk.gray(n("whats_next",t)));for(const o of C(r.unlockedNext,60))console.log(chalk.bold.yellow(" ║ ")+" "+chalk.white(o));console.log(chalk.bold.yellow(" ║")),console.log(chalk.bold.yellow(" ╰"+g("═"))),console.log(),c?(console.log(chalk.gray(" "+n("demo_complete",t))),console.log(),console.log(chalk.gray(" "+n("unlock_full",t)+" ")+chalk.white(n("curriculum_name",t))+chalk.gray(",")),console.log(chalk.gray(" "+n("contact_team_leader",t)+" ")+chalk.bold.yellow("EA")+chalk.gray(" "+n("learn_token",t))),console.log(chalk.gray(" "+n("or_email",t)+" ")+chalk.cyan("australia@icoa2026.au")+chalk.gray(" "+n("for_partnership",t))),console.log(),console.log(chalk.gray(" "+n("type_quit",t)+" ")+chalk.bold.green("quit")+chalk.gray(" "+n("to_exit",t)+" ")+chalk.bold.green("status")+chalk.gray(" "+n("for_dashboard",t)))):(console.log(chalk.gray(" ")+chalk.bold.green("ok")+chalk.gray(" / ")+chalk.bold.green("next")+chalk.gray(" "+n("continue_to_next",t))),console.log(chalk.gray(" status "+n("status_full",t))),console.log(chalk.gray(" card N jump to a card by number")),console.log(chalk.gray(" quit "+n("quit_desc",t)))),console.log()}export function renderStatus(o,e){const l=s(),t=e.cardsCompleted.length,r=o.totalCards,c=Object.values(e.mcqResults),a=c.filter(o=>o.correct).length;console.log(),console.log(chalk.cyan(" ╭─ ")+chalk.bold.white(n("status_title",l))+" "+chalk.cyan("─".repeat(25))),m(),h(" "+chalk.gray(n("total_progress",l)+" ")+i(t,r)+chalk.gray(` (${t}/${r})`)),h(" "+chalk.gray(n("streak",l)+" ")+d(e.streakDays)+chalk.gray(` (${n("longest",l)} ${e.longestStreak})`)),h(" "+chalk.gray(n("mcq_accuracy",l)+" ")+chalk.white(`${a}/${c.length}`)),h(" "+chalk.gray(n("practicals_done",l)+" ")+chalk.white(`${e.practicalsCompleted.length}`)),h(" "+chalk.gray(n("bookmarked",l)+" ")+chalk.white(`${e.bookmarks.length}`)),m();const g=n("module",l).replace(":","").trim();for(const n of o.modules){const[o,l]=n.cardRange,t=e.cardsCompleted.filter(e=>e>=o&&e<=l).length,r=l-o+1;h(" "+(t===r?chalk.green("✓"):t>0?chalk.yellow("▶"):chalk.gray("□"))+" "+chalk.gray(`${g} ${n.number}: `)+chalk.white(`${t}/${r}`)+chalk.gray(" "+n.name))}if(m(),e.achievements.length>0){h(" "+chalk.gray(n("achievements",l)));for(const o of e.achievements)h(" "+chalk.bold.yellow("★ ")+chalk.white(o))}else h(" "+chalk.gray(n("achievements_none",l)));m(),u()}
|