dsh-aha-tutor 1.4.0 → 1.6.0

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.
@@ -20,6 +20,716 @@ function assertId(value, prefix) {
20
20
  return value;
21
21
  }
22
22
  //#endregion
23
+ //#region src/core/expr.js
24
+ const EXPR_LIMITS = Object.freeze({
25
+ chars: 240,
26
+ nodes: 400,
27
+ depth: 40
28
+ });
29
+ var ExprError = class extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "ExprError";
33
+ }
34
+ };
35
+ const FUNCTIONS = Object.freeze({
36
+ sin: [1, Math.sin],
37
+ cos: [1, Math.cos],
38
+ tan: [1, Math.tan],
39
+ cot: [1, (x) => 1 / Math.tan(x)],
40
+ sec: [1, (x) => 1 / Math.cos(x)],
41
+ csc: [1, (x) => 1 / Math.sin(x)],
42
+ asin: [1, Math.asin],
43
+ acos: [1, Math.acos],
44
+ atan: [1, Math.atan],
45
+ arcsin: [1, Math.asin],
46
+ arccos: [1, Math.acos],
47
+ arctan: [1, Math.atan],
48
+ atan2: [2, Math.atan2],
49
+ sinh: [1, Math.sinh],
50
+ cosh: [1, Math.cosh],
51
+ tanh: [1, Math.tanh],
52
+ asinh: [1, Math.asinh],
53
+ acosh: [1, Math.acosh],
54
+ atanh: [1, Math.atanh],
55
+ exp: [1, Math.exp],
56
+ ln: [1, Math.log],
57
+ log: [1, Math.log10],
58
+ lg: [1, Math.log10],
59
+ log10: [1, Math.log10],
60
+ log2: [1, Math.log2],
61
+ sqrt: [1, Math.sqrt],
62
+ cbrt: [1, Math.cbrt],
63
+ abs: [1, Math.abs],
64
+ sign: [1, Math.sign],
65
+ floor: [1, Math.floor],
66
+ ceil: [1, Math.ceil],
67
+ round: [1, Math.round],
68
+ min: [-2, Math.min],
69
+ max: [-2, Math.max],
70
+ pow: [2, Math.pow]
71
+ });
72
+ const CONSTANTS = Object.freeze({
73
+ pi: Math.PI,
74
+ π: Math.PI,
75
+ e: Math.E
76
+ });
77
+ const isFunctionName = (name) => Object.hasOwn(FUNCTIONS, name);
78
+ const isConstantName = (name) => Object.hasOwn(CONSTANTS, name);
79
+ const REPLACEMENTS = [
80
+ [/[−–—]/gu, "-"],
81
+ [/[×·⋅∙]/gu, "*"],
82
+ [/÷/gu, "/"],
83
+ [/\*\*/gu, "^"],
84
+ [/²/gu, "^2"],
85
+ [/³/gu, "^3"],
86
+ [/[(]/gu, "("],
87
+ [/[)]/gu, ")"],
88
+ [/[,]/gu, ","]
89
+ ];
90
+ function tokenize$1(source) {
91
+ let input = String(source ?? "");
92
+ for (const [pattern, value] of REPLACEMENTS) input = input.replace(pattern, value);
93
+ const tokens = [];
94
+ let at = 0;
95
+ while (at < input.length) {
96
+ const char = input[at];
97
+ if (/\s/u.test(char)) {
98
+ at += 1;
99
+ continue;
100
+ }
101
+ const number = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/u.exec(input.slice(at));
102
+ if (number) {
103
+ tokens.push({
104
+ type: "num",
105
+ value: Number(number[0])
106
+ });
107
+ at += number[0].length;
108
+ continue;
109
+ }
110
+ const name = /^[\p{L}_][\p{L}\p{N}_]*/u.exec(input.slice(at));
111
+ if (name) {
112
+ tokens.push({
113
+ type: "name",
114
+ value: name[0]
115
+ });
116
+ at += name[0].length;
117
+ continue;
118
+ }
119
+ if ("+-*/^(),|√".includes(char)) {
120
+ tokens.push({ type: char });
121
+ at += 1;
122
+ continue;
123
+ }
124
+ throw new ExprError(`Unexpected "${char}"`);
125
+ }
126
+ return tokens;
127
+ }
128
+ function parseExpr(source, names = []) {
129
+ const text = String(source ?? "");
130
+ if (!text.trim()) throw new ExprError("Empty formula");
131
+ if (text.length > EXPR_LIMITS.chars) throw new ExprError("Formula too long");
132
+ const known = new Set(names);
133
+ const tokens = tokenize$1(text);
134
+ let index = 0;
135
+ let nodes = 0;
136
+ let depth = 0;
137
+ const peek = () => tokens[index];
138
+ const take = (type) => {
139
+ const token = tokens[index];
140
+ if (!token || type && token.type !== type) throw new ExprError(type ? `Expected "${type}"` : "Unexpected end");
141
+ index += 1;
142
+ return token;
143
+ };
144
+ const node = (value) => {
145
+ nodes += 1;
146
+ if (nodes > EXPR_LIMITS.nodes) throw new ExprError("Formula too long");
147
+ return value;
148
+ };
149
+ const nested = (parse) => {
150
+ depth += 1;
151
+ if (depth > EXPR_LIMITS.depth) throw new ExprError("Formula nested too deeply");
152
+ try {
153
+ return parse();
154
+ } finally {
155
+ depth -= 1;
156
+ }
157
+ };
158
+ const startsPrimary = (token) => token && (token.type === "num" || token.type === "name" || token.type === "(" || token.type === "√");
159
+ function sum() {
160
+ let left = product();
161
+ while (peek()?.type === "+" || peek()?.type === "-") {
162
+ const op = take().type;
163
+ left = node({
164
+ k: "bin",
165
+ op,
166
+ a: left,
167
+ b: product()
168
+ });
169
+ }
170
+ return left;
171
+ }
172
+ function product() {
173
+ let left = unary();
174
+ for (;;) {
175
+ const token = peek();
176
+ if (token?.type === "*" || token?.type === "/") {
177
+ take();
178
+ left = node({
179
+ k: "bin",
180
+ op: token.type,
181
+ a: left,
182
+ b: unary()
183
+ });
184
+ } else if (startsPrimary(token)) left = node({
185
+ k: "bin",
186
+ op: "*",
187
+ a: left,
188
+ b: power()
189
+ });
190
+ else return left;
191
+ }
192
+ }
193
+ function unary() {
194
+ const token = peek();
195
+ if (token?.type === "-") {
196
+ take();
197
+ return node({
198
+ k: "neg",
199
+ a: nested(unary)
200
+ });
201
+ }
202
+ if (token?.type === "+") {
203
+ take();
204
+ return nested(unary);
205
+ }
206
+ return power();
207
+ }
208
+ function power() {
209
+ const base = primary();
210
+ if (peek()?.type !== "^") return base;
211
+ take();
212
+ return node({
213
+ k: "bin",
214
+ op: "^",
215
+ a: base,
216
+ b: nested(unary)
217
+ });
218
+ }
219
+ function call(name) {
220
+ const [arity] = FUNCTIONS[name];
221
+ take("(");
222
+ const args = [nested(sum)];
223
+ while (peek()?.type === ",") {
224
+ take();
225
+ args.push(nested(sum));
226
+ }
227
+ take(")");
228
+ if (arity > 0 ? args.length !== arity : args.length < -arity) throw new ExprError(`${name} takes ${Math.abs(arity)} argument${Math.abs(arity) > 1 ? "s" : ""}`);
229
+ return node({
230
+ k: "call",
231
+ f: name,
232
+ args
233
+ });
234
+ }
235
+ function named(value) {
236
+ if (known.has(value)) return node({
237
+ k: "var",
238
+ n: value
239
+ });
240
+ if (isConstantName(value)) return node({
241
+ k: "num",
242
+ v: CONSTANTS[value]
243
+ });
244
+ if (isFunctionName(value)) {
245
+ if (peek()?.type !== "(") throw new ExprError(`Write ${value}(…) with parentheses`);
246
+ return call(value);
247
+ }
248
+ const parts = splitName(value);
249
+ if (!parts) throw new ExprError(`Unknown name "${value}"`);
250
+ let result = null;
251
+ for (const [partIndex, part] of parts.entries()) {
252
+ let factor;
253
+ if (isFunctionName(part)) {
254
+ if (partIndex !== parts.length - 1 || peek()?.type !== "(") throw new ExprError(`Unknown name "${value}"`);
255
+ factor = call(part);
256
+ } else factor = node(known.has(part) ? {
257
+ k: "var",
258
+ n: part
259
+ } : {
260
+ k: "num",
261
+ v: CONSTANTS[part]
262
+ });
263
+ result = result ? node({
264
+ k: "bin",
265
+ op: "*",
266
+ a: result,
267
+ b: factor
268
+ }) : factor;
269
+ }
270
+ return result;
271
+ }
272
+ function splitName(value) {
273
+ const candidates = [
274
+ ...known,
275
+ ...Object.keys(CONSTANTS),
276
+ ...Object.keys(FUNCTIONS)
277
+ ].sort((a, b) => b.length - a.length);
278
+ const parts = [];
279
+ let rest = value;
280
+ while (rest) {
281
+ const found = candidates.find((candidate) => rest.startsWith(candidate));
282
+ if (!found) return null;
283
+ parts.push(found);
284
+ rest = rest.slice(found.length);
285
+ }
286
+ return parts.length > 1 ? parts : null;
287
+ }
288
+ function primary() {
289
+ const token = take();
290
+ if (token.type === "num") return node({
291
+ k: "num",
292
+ v: token.value
293
+ });
294
+ if (token.type === "name") return named(token.value);
295
+ if (token.type === "(") {
296
+ const inner = nested(sum);
297
+ take(")");
298
+ return inner;
299
+ }
300
+ if (token.type === "|") {
301
+ const inner = nested(sum);
302
+ take("|");
303
+ return node({
304
+ k: "call",
305
+ f: "abs",
306
+ args: [inner]
307
+ });
308
+ }
309
+ if (token.type === "√") return node({
310
+ k: "call",
311
+ f: "sqrt",
312
+ args: [nested(power)]
313
+ });
314
+ throw new ExprError(`Unexpected "${token.type}"`);
315
+ }
316
+ const tree = sum();
317
+ if (index < tokens.length) throw new ExprError(`Unexpected "${tokens[index].type === "name" || tokens[index].type === "num" ? tokens[index].value : tokens[index].type}"`);
318
+ return tree;
319
+ }
320
+ function compileExpr(tree, names = []) {
321
+ const slot = new Map(names.map((name, position) => [name, position]));
322
+ const build = (item) => {
323
+ switch (item.k) {
324
+ case "num": {
325
+ const value = item.v;
326
+ return () => value;
327
+ }
328
+ case "var": {
329
+ const position = slot.get(item.n);
330
+ if (position === void 0) throw new ExprError(`Unknown name "${item.n}"`);
331
+ return (values) => values[position];
332
+ }
333
+ case "neg": {
334
+ const a = build(item.a);
335
+ return (values) => -a(values);
336
+ }
337
+ case "bin": {
338
+ const a = build(item.a);
339
+ const b = build(item.b);
340
+ if (item.op === "+") return (values) => a(values) + b(values);
341
+ if (item.op === "-") return (values) => a(values) - b(values);
342
+ if (item.op === "*") return (values) => a(values) * b(values);
343
+ if (item.op === "/") return (values) => a(values) / b(values);
344
+ return (values) => power(a(values), b(values));
345
+ }
346
+ case "call": {
347
+ const implementation = FUNCTIONS[item.f][1];
348
+ const args = item.args.map(build);
349
+ if (args.length === 1) {
350
+ const [a] = args;
351
+ return (values) => implementation(a(values));
352
+ }
353
+ return (values) => implementation(...args.map((arg) => arg(values)));
354
+ }
355
+ default: throw new ExprError("Bad formula");
356
+ }
357
+ };
358
+ return build(tree);
359
+ }
360
+ function power(base, exponent) {
361
+ const value = base ** exponent;
362
+ if (!Number.isNaN(value) || base >= 0 || !Number.isFinite(exponent)) return value;
363
+ for (const denominator of [
364
+ 3,
365
+ 5,
366
+ 7,
367
+ 9
368
+ ]) {
369
+ const numerator = Math.round(exponent * denominator);
370
+ if (Math.abs(numerator / denominator - exponent) < 1e-9) return (-((-base) ** (1 / denominator))) ** numerator;
371
+ }
372
+ return value;
373
+ }
374
+ function constantValue(value) {
375
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
376
+ if (typeof value !== "string" || !value.trim()) return null;
377
+ try {
378
+ const result = compileExpr(parseExpr(value))([]);
379
+ return Number.isFinite(result) ? result : null;
380
+ } catch {
381
+ return null;
382
+ }
383
+ }
384
+ const SUPERSCRIPT = {
385
+ 0: "⁰",
386
+ 1: "¹",
387
+ 2: "²",
388
+ 3: "³",
389
+ 4: "⁴",
390
+ 5: "⁵",
391
+ 6: "⁶",
392
+ 7: "⁷",
393
+ 8: "⁸",
394
+ 9: "⁹",
395
+ "-": "⁻"
396
+ };
397
+ const DISPLAY_NAMES = {
398
+ pi: "π",
399
+ theta: "θ",
400
+ omega: "ω",
401
+ phi: "φ",
402
+ alpha: "α",
403
+ beta: "β",
404
+ lambda: "λ",
405
+ mu: "μ",
406
+ sigma: "σ",
407
+ tau: "τ"
408
+ };
409
+ const PRECEDENCE = {
410
+ "+": 1,
411
+ "-": 1,
412
+ "*": 2,
413
+ "/": 2,
414
+ "^": 4
415
+ };
416
+ function formatNumber(value) {
417
+ if (Math.abs(value - Math.PI) < 1e-12) return "π";
418
+ if (Math.abs(value - Math.E) < 1e-12) return "e";
419
+ return Number(value.toPrecision(6)).toString();
420
+ }
421
+ const superscript = (value) => [...String(value)].map((char) => SUPERSCRIPT[char]).join("");
422
+ const symbol = (value) => [...value].length === 1 && /\p{L}/u.test(value);
423
+ function exprToText(tree, display = DISPLAY_NAMES) {
424
+ const show = (name) => display[name] ?? name;
425
+ const write = (item, parent = 0) => {
426
+ switch (item.k) {
427
+ case "num": return formatNumber(item.v);
428
+ case "var": return show(item.n);
429
+ case "neg": {
430
+ const inner = write(item.a, 3);
431
+ return parent > 2 ? `(−${inner})` : `−${inner}`;
432
+ }
433
+ case "call":
434
+ if (item.f === "abs") return `|${write(item.args[0])}|`;
435
+ if (item.f === "sqrt") return `√(${write(item.args[0])})`;
436
+ return `${item.f}(${item.args.map((arg) => write(arg)).join(", ")})`;
437
+ case "bin": {
438
+ const own = PRECEDENCE[item.op];
439
+ let out;
440
+ if (item.op === "^") {
441
+ const base = write(item.a, 5);
442
+ const integer = (value) => value.k === "num" && Number.isInteger(value.v) && Math.abs(value.v) < 100;
443
+ const top = write(item.b);
444
+ out = `${base}${integer(item.b) ? superscript(item.b.v) : item.b.k === "neg" && integer(item.b.a) ? `⁻${superscript(item.b.a.v)}` : symbol(top) ? `^${top}` : `^(${top})`}`;
445
+ } else if (item.op === "*") {
446
+ const a = write(item.a, own);
447
+ const b = write(item.b, own);
448
+ out = item.a.k === "num" && /\d$/u.test(a) && /^[\p{L}(√|]/u.test(b) || symbol(a) && (symbol(b) || b.startsWith("(")) || item.a.k !== "call" && /^[\d\p{L}]+$/u.test(a) && /\p{L}$/u.test(a) && symbol(b) || item.a.k !== "call" && a.endsWith(")") && b.startsWith("(") ? `${a}${b}` : `${a}·${b}`;
449
+ } else {
450
+ const a = write(item.a, own);
451
+ const b = write(item.b, item.op === "+" ? own : own + .5);
452
+ out = `${a} ${item.op === "-" ? "−" : item.op} ${b}`;
453
+ }
454
+ return own < parent ? `(${out})` : out;
455
+ }
456
+ default: return "";
457
+ }
458
+ };
459
+ return write(tree);
460
+ }
461
+ //#endregion
462
+ //#region src/core/plot.js
463
+ const PLOT_LIMITS = Object.freeze({
464
+ fns: 4,
465
+ params: 4,
466
+ points: 8,
467
+ lines: 6,
468
+ shades: 3,
469
+ tangents: 3,
470
+ samples: 480
471
+ });
472
+ const clip = (value, max) => value === null || value === void 0 ? "" : String(value).replace(/\u0000/gu, "").trim().slice(0, max);
473
+ const list$1 = (value, max) => (Array.isArray(value) ? value : value && typeof value === "object" ? [value] : []).slice(0, max);
474
+ const clamp$1 = (value, min, max) => Math.min(max, Math.max(min, value));
475
+ const mentionsPi = (value) => /pi|π/iu.test(String(value ?? ""));
476
+ const VARIABLES = Object.freeze({
477
+ fx: ["x", "t"],
478
+ param: [
479
+ "t",
480
+ "θ",
481
+ "theta"
482
+ ],
483
+ polar: [
484
+ "θ",
485
+ "theta",
486
+ "t"
487
+ ]
488
+ });
489
+ const RESERVED = /* @__PURE__ */ new Set([
490
+ "x",
491
+ "y",
492
+ "r",
493
+ "θ",
494
+ "theta"
495
+ ]);
496
+ function curveVariables(kind, paramNames) {
497
+ return VARIABLES[kind].filter((name) => !paramNames.includes(name));
498
+ }
499
+ function niceStep(raw) {
500
+ const power = 10 ** Math.floor(Math.log10(raw));
501
+ const unit = raw / power;
502
+ return (unit >= 5 ? 5 : unit >= 2 ? 2 : 1) * power;
503
+ }
504
+ function readRange(value) {
505
+ const pair = Array.isArray(value) ? value : value && typeof value === "object" ? Array.isArray(value.range) ? value.range : [value.min, value.max] : null;
506
+ if (!pair || pair.length < 2) return null;
507
+ const a = constantValue(pair[0]);
508
+ const b = constantValue(pair[1]);
509
+ return a !== null && b !== null && b > a ? [a, b] : null;
510
+ }
511
+ function normalizeParams(raw) {
512
+ const seen = /* @__PURE__ */ new Set();
513
+ const params = [];
514
+ for (const param of list$1(raw, PLOT_LIMITS.params)) {
515
+ const name = clip(param?.name ?? param?.symbol ?? param?.id, 12);
516
+ if (!/^\p{L}[\p{L}\p{N}_]{0,11}$/u.test(name) || isFunctionName(name) || isConstantName(name) || RESERVED.has(name) || seen.has(name)) continue;
517
+ const min = constantValue(param.min);
518
+ const max = constantValue(param.max);
519
+ if (min === null || max === null || !(max > min)) continue;
520
+ const start = constantValue(param.value ?? param.default ?? param.initial);
521
+ let step = constantValue(param.step);
522
+ if (!(step > 0) || step > max - min) step = niceStep((max - min) / 100);
523
+ seen.add(name);
524
+ params.push({
525
+ name,
526
+ value: clamp$1(start ?? (min + max) / 2, min, max),
527
+ min,
528
+ max,
529
+ step,
530
+ label: clip(param.label ?? param.title, 30)
531
+ });
532
+ }
533
+ return params;
534
+ }
535
+ const formulaText = (value) => clip(value, 240).replace(/^[\p{L}](?:\s*\(\s*[\p{L}]+\s*\))?\s*=(?!=)/u, "").trim();
536
+ function checks(source, names) {
537
+ try {
538
+ parseExpr(source, names);
539
+ return true;
540
+ } catch {
541
+ return false;
542
+ }
543
+ }
544
+ function normalizeCurve(raw, paramNames) {
545
+ const curve = typeof raw === "string" ? { y: raw } : raw;
546
+ if (!curve || typeof curve !== "object") return null;
547
+ const name = clip(curve.name ?? curve.label, 40);
548
+ const known = (kind) => [...curveVariables(kind, paramNames), ...paramNames];
549
+ if (curve.r !== void 0) {
550
+ const r = formulaText(curve.r);
551
+ if (!checks(r, known("polar"))) return null;
552
+ return {
553
+ kind: "polar",
554
+ r,
555
+ t: readRange(curve.t ?? curve.theta ?? curve.θ ?? curve.range) ?? [0, 2 * Math.PI],
556
+ name
557
+ };
558
+ }
559
+ if (typeof curve.x === "string" && typeof curve.y === "string") {
560
+ const x = formulaText(curve.x);
561
+ const y = formulaText(curve.y);
562
+ if (!checks(x, known("param")) || !checks(y, known("param"))) return null;
563
+ return {
564
+ kind: "param",
565
+ x,
566
+ y,
567
+ t: readRange(curve.t ?? curve.range) ?? [0, 2 * Math.PI],
568
+ name
569
+ };
570
+ }
571
+ const y = formulaText(curve.y ?? curve.fn ?? curve.f ?? curve.expr ?? curve.formula);
572
+ if (!y || !checks(y, known("fx"))) return null;
573
+ const domain = readRange(curve.domain ?? (Array.isArray(curve.x) ? curve.x : null));
574
+ return {
575
+ kind: "fx",
576
+ y,
577
+ ...domain ? { domain } : {},
578
+ name
579
+ };
580
+ }
581
+ function fxIndex(value, fns, fallback = true) {
582
+ const index = Number(value);
583
+ if (Number.isInteger(index) && fns[index]?.kind === "fx") return index;
584
+ if (value !== void 0 && value !== null && value !== "") return null;
585
+ if (!fallback) return null;
586
+ const first = fns.findIndex((fn) => fn.kind === "fx");
587
+ return first >= 0 ? first : null;
588
+ }
589
+ function normalizePlot(raw) {
590
+ if (!raw || typeof raw !== "object") return null;
591
+ const params = normalizeParams(raw.params ?? raw.parameters ?? raw.sliders);
592
+ const paramNames = params.map((param) => param.name);
593
+ const single = typeof raw.y === "string" || raw.fn !== void 0 || raw.expr !== void 0 || raw.f !== void 0 || raw.formula !== void 0 ? [{
594
+ ...raw,
595
+ x: void 0,
596
+ name: raw.name ?? raw.label
597
+ }] : [];
598
+ const fns = list$1(raw.fns ?? raw.functions ?? raw.curves ?? single, PLOT_LIMITS.fns * 2).map((curve) => normalizeCurve(curve, paramNames)).filter(Boolean).slice(0, PLOT_LIMITS.fns);
599
+ if (!fns.length) return null;
600
+ const xSource = raw.x !== void 0 && typeof raw.x !== "string" ? raw.x : raw.xRange ?? raw.domain ?? (raw.xMin !== void 0 ? [raw.xMin, raw.xMax] : void 0);
601
+ const ySource = raw.y !== void 0 && typeof raw.y !== "string" ? raw.y : raw.yRange ?? (raw.yMin !== void 0 ? [raw.yMin, raw.yMax] : void 0);
602
+ const x = readRange(xSource) ?? (fns.some((fn) => fn.kind === "fx") ? [-5, 5] : null);
603
+ const y = readRange(ySource);
604
+ const points = [];
605
+ for (const point of list$1(raw.points ?? raw.marks, PLOT_LIMITS.points)) {
606
+ const px = constantValue(point?.x);
607
+ if (px === null) continue;
608
+ const py = constantValue(point?.y);
609
+ const fn = py === null ? fxIndex(point?.fn, fns) : null;
610
+ if (py === null && fn === null) continue;
611
+ points.push({
612
+ x: px,
613
+ ...py === null ? { fn } : { y: py },
614
+ label: clip(point?.label ?? point?.name, 30)
615
+ });
616
+ }
617
+ const lines = [];
618
+ for (const line of list$1(raw.lines ?? raw.guides, PLOT_LIMITS.lines)) {
619
+ const lx = constantValue(line?.x);
620
+ const ly = lx === null ? constantValue(line?.y) : null;
621
+ if (lx === null && ly === null) continue;
622
+ lines.push({
623
+ ...lx === null ? { y: ly } : { x: lx },
624
+ label: clip(line?.label, 30)
625
+ });
626
+ }
627
+ const shades = [];
628
+ for (const shade of list$1(raw.shade ?? raw.shades ?? raw.areas, PLOT_LIMITS.shades)) {
629
+ const fn = fxIndex(shade?.fn, fns);
630
+ const from = constantValue(shade?.from ?? shade?.a);
631
+ const to = constantValue(shade?.to ?? shade?.b);
632
+ if (fn === null || from === null || to === null || !(to > from)) continue;
633
+ const fn2 = fxIndex(shade?.fn2 ?? shade?.to_fn ?? shade?.below, fns, false);
634
+ shades.push({
635
+ fn,
636
+ from,
637
+ to,
638
+ ...fn2 !== null && fn2 !== fn ? { fn2 } : {},
639
+ label: clip(shade?.label, 30)
640
+ });
641
+ }
642
+ const tangents = [];
643
+ for (const tangent of list$1(raw.tangents ?? raw.tangent, PLOT_LIMITS.tangents)) {
644
+ const fn = fxIndex(tangent?.fn, fns);
645
+ const at = constantValue(tangent?.x ?? tangent?.at);
646
+ if (fn === null || at === null) continue;
647
+ tangents.push({
648
+ fn,
649
+ x: at,
650
+ label: clip(tangent?.label, 30)
651
+ });
652
+ }
653
+ return {
654
+ title: clip(raw.title, 120),
655
+ caption: clip(raw.caption, 200),
656
+ x: x ? {
657
+ min: x[0],
658
+ max: x[1]
659
+ } : null,
660
+ y: y ? {
661
+ min: y[0],
662
+ max: y[1]
663
+ } : null,
664
+ xLabel: clip(raw.xLabel ?? raw.x?.label, 40),
665
+ yLabel: clip(raw.yLabel ?? raw.y?.label, 40),
666
+ xPi: mentionsPi(JSON.stringify(xSource ?? "")) || x !== null && isPiMultiple(x[0]) && isPiMultiple(x[1]) && !(Number.isInteger(x[0]) && Number.isInteger(x[1])),
667
+ equal: raw.equal === true || raw.aspect === "equal",
668
+ fns,
669
+ params,
670
+ points,
671
+ lines,
672
+ shades,
673
+ tangents
674
+ };
675
+ }
676
+ const isPiMultiple = (value) => Math.abs(value / (Math.PI / 4) - Math.round(value / (Math.PI / 4))) < .001;
677
+ function readable(source, plot) {
678
+ try {
679
+ return exprToText(parseExpr(source, [.../* @__PURE__ */ new Set([...Object.values(VARIABLES).flat(), ...plot.params.map((param) => param.name)])]));
680
+ } catch {
681
+ return source;
682
+ }
683
+ }
684
+ const number = (value) => {
685
+ if (!Number.isFinite(value)) return String(value);
686
+ const ratio = value / Math.PI;
687
+ if (value !== 0 && Math.abs(ratio * 4 - Math.round(ratio * 4)) < 1e-6) {
688
+ const quarters = Math.round(ratio * 4);
689
+ const [top, bottom] = quarters % 4 === 0 ? [quarters / 4, 1] : quarters % 2 === 0 ? [quarters / 2, 2] : [quarters, 4];
690
+ const head = top === 1 ? "π" : top === -1 ? "−π" : `${top}π`.replace("-", "−");
691
+ return bottom === 1 ? head : `${head}/${bottom}`;
692
+ }
693
+ return Number(value.toPrecision(4)).toString().replace("-", "−");
694
+ };
695
+ function plotCurveLabels(plot) {
696
+ return plot.fns.map((fn) => {
697
+ if (fn.kind === "polar") return `r = ${readable(fn.r, plot)}`;
698
+ if (fn.kind === "param") return `x = ${readable(fn.x, plot)}, y = ${readable(fn.y, plot)}`;
699
+ return `y = ${readable(fn.y, plot)}`;
700
+ });
701
+ }
702
+ function plotToText(plot) {
703
+ const parts = [plot.title || plot.caption].filter(Boolean);
704
+ const curves = plotCurveLabels(plot).map((label, index) => {
705
+ const fn = plot.fns[index];
706
+ const range = fn.kind === "fx" ? plot.x ? `, x ${number(fn.domain?.[0] ?? plot.x.min)} … ${number(fn.domain?.[1] ?? plot.x.max)}` : "" : `, ${fn.kind === "polar" ? "θ" : "t"} ${number(fn.t[0])} … ${number(fn.t[1])}`;
707
+ return `${fn.name ? `${fn.name}: ` : ""}${label}${range}`;
708
+ });
709
+ parts.push(...curves);
710
+ if (plot.params.length) parts.push(plot.params.map((param) => `${param.label || param.name} = ${number(param.value)} (${number(param.min)} … ${number(param.max)})`).join("; "));
711
+ return parts.join("\n");
712
+ }
713
+ function plotToSource(plot) {
714
+ return JSON.stringify({
715
+ fns: plot.fns.map((fn) => Object.fromEntries(Object.entries(fn).filter(([, value]) => value !== "" && value !== void 0))),
716
+ ...plot.params.length ? { params: plot.params.map(({ name, value, min, max, label }) => ({
717
+ name,
718
+ value,
719
+ min,
720
+ max,
721
+ ...label ? { label } : {}
722
+ })) } : {},
723
+ ...plot.x ? { x: [plot.x.min, plot.x.max] } : {},
724
+ ...plot.y ? { y: [plot.y.min, plot.y.max] } : {},
725
+ ...plot.points.length ? { points: plot.points } : {},
726
+ ...plot.lines.length ? { lines: plot.lines } : {},
727
+ ...plot.shades.length ? { shade: plot.shades } : {},
728
+ ...plot.tangents.length ? { tangents: plot.tangents } : {},
729
+ ...plot.title ? { title: plot.title } : {}
730
+ });
731
+ }
732
+ //#endregion
23
733
  //#region src/core/course-model.js
24
734
  const LIMITS = Object.freeze({
25
735
  questions: 6,
@@ -285,6 +995,10 @@ function normalizeItem(raw, index = 0, conceptIds = []) {
285
995
  };
286
996
  const figure = text(raw?.svg ?? raw?.figure, 16e3);
287
997
  if (/^<svg[\s>]/iu.test(figure)) item.svg = figure;
998
+ else if (raw?.plot && typeof raw.plot === "object") {
999
+ const plot = normalizePlot(raw.plot);
1000
+ if (plot) item.plot = plot;
1001
+ }
288
1002
  if (kind === "choice" || kind === "multi" || kind === "order") {
289
1003
  item.options = list(raw?.options, LIMITS.options4, (option, optionIndex) => {
290
1004
  const label = text(typeof option === "string" ? option : option?.text ?? option?.label, 400);
@@ -492,6 +1206,7 @@ const OP_TYPES = Object.freeze([
492
1206
  "list",
493
1207
  "svg",
494
1208
  "chart",
1209
+ "plot",
495
1210
  "table",
496
1211
  "code",
497
1212
  "note",
@@ -516,7 +1231,8 @@ const OP_ALIASES = Object.freeze({
516
1231
  drawing: "svg",
517
1232
  figure: "svg",
518
1233
  graph: "chart",
519
- plot: "chart",
1234
+ function: "plot",
1235
+ curve: "plot",
520
1236
  callout: "note",
521
1237
  highlight: "mark",
522
1238
  underline: "mark",
@@ -612,13 +1328,22 @@ function normalizeOp(raw, index) {
612
1328
  wide: raw?.wide === true
613
1329
  };
614
1330
  }
615
- case "chart": {
1331
+ case "chart":
1332
+ case "plot": {
1333
+ if (raw?.fns ?? raw?.functions ?? raw?.curves ?? (typeof raw?.y === "string" || raw?.fn !== void 0 || raw?.expr !== void 0 ? raw : null)) {
1334
+ const plot = normalizePlot(raw);
1335
+ if (plot) return {
1336
+ id,
1337
+ t: "plot",
1338
+ ...plot
1339
+ };
1340
+ }
616
1341
  const kind = CHART_KINDS.includes(raw?.kind) ? raw.kind : "line";
617
1342
  const { series, data } = chartSeries(raw);
618
1343
  if (!series.length && !data.length) return null;
619
1344
  return {
620
1345
  id,
621
- t,
1346
+ t: "chart",
622
1347
  kind,
623
1348
  title: text(raw?.title, 120),
624
1349
  xLabel: text(raw?.xLabel ?? raw?.x?.label, 60),
@@ -754,6 +1479,14 @@ function normalizeBoard(raw, { conceptIds = [] } = {}) {
754
1479
  steps
755
1480
  };
756
1481
  }
1482
+ const CJK = /[぀-ヿ㐀-䶿一-鿿가-힯]/gu;
1483
+ function estimateSpeechMs(value, rate = 1) {
1484
+ const content = String(value ?? "");
1485
+ const cjk = (content.match(CJK) ?? []).length;
1486
+ const words = content.replace(CJK, " ").split(/\s+/u).filter(Boolean).length;
1487
+ const seconds = cjk / 4.4 + words / 2.6;
1488
+ return Math.round(seconds * 1e3 / Math.max(.5, Math.min(2, rate || 1)));
1489
+ }
757
1490
  function boardToPlainText(board, { maxChars = 8e3, uptoStep } = {}) {
758
1491
  const lines = [];
759
1492
  for (const [index, step] of board.steps.entries()) {
@@ -763,6 +1496,7 @@ function boardToPlainText(board, { maxChars = 8e3, uptoStep } = {}) {
763
1496
  else if (op.t === "list") lines.push(...op.items.map((item) => ` • ${item}`));
764
1497
  else if (op.t === "table") lines.push(` | ${op.head.join(" | ")} |`, ...op.rows.map((row) => ` | ${row.join(" | ")} |`));
765
1498
  else if (op.t === "code") lines.push(` \`\`\`${op.lang}\n${op.code}\n \`\`\``);
1499
+ else if (op.t === "plot") lines.push(` (plot: ${plotToSource(op)})`);
766
1500
  else if (op.text) lines.push(` ${op.text}`);
767
1501
  else if (op.caption) lines.push(` (${op.t}: ${op.caption})`);
768
1502
  }
@@ -1381,9 +2115,10 @@ var TutorError = class extends Error {
1381
2115
  }
1382
2116
  };
1383
2117
  const REPAIR = "Your previous reply could not be parsed as JSON. Reply again with the complete answer as valid JSON only — no code fences, no commentary.";
1384
- async function generateJson(llm, { purpose = "teach", prompt, maxTokens = 8e3, images, signal, normalize = (value) => value, onText }) {
2118
+ async function generateJson(llm, { purpose = "teach", effort, prompt, maxTokens = 8e3, images, signal, normalize = (value) => value, onText }) {
1385
2119
  const request = {
1386
2120
  purpose,
2121
+ ...effort ? { effort } : {},
1387
2122
  system: prompt.system,
1388
2123
  user: prompt.user,
1389
2124
  images,
@@ -1400,12 +2135,8 @@ async function generateJson(llm, { purpose = "teach", prompt, maxTokens = 8e3, i
1400
2135
  if (signal?.aborted) throw signal.reason ?? error;
1401
2136
  const retryUser = `${prompt.user}\n\n<previous_reply>\n${String(first).slice(0, 12e3)}\n</previous_reply>\n\n${REPAIR}${error instanceof JsonExtractError ? "" : `\nProblem: ${error.message}`}`;
1402
2137
  const second = await llm.complete({
1403
- purpose,
1404
- system: prompt.system,
1405
- user: retryUser,
1406
- images,
1407
- maxTokens,
1408
- signal
2138
+ ...request,
2139
+ user: retryUser
1409
2140
  });
1410
2141
  try {
1411
2142
  return normalize(extractJson(second));
@@ -1614,7 +2345,8 @@ Board operations (each is a JSON object with field "t"; give an op an "id" only
1614
2345
  - {"t":"math","tex":"…","text":"…"} one display formula in KaTeX-compatible LaTeX; "text" is a plain-Unicode reading of it (e.g. "θ(t) = θ₀·cos(ωt + φ)")
1615
2346
  - {"t":"list","items":["…","…"],"ordered":false}
1616
2347
  - {"t":"svg","svg":"<svg viewBox=\\"0 0 480 300\\" xmlns=\\"http://www.w3.org/2000/svg\\">…</svg>","caption":"…"} a clean hand-drawn-style diagram: viewBox at most 640×420, strokes and simple shapes, <text> labels (font-size 14–18), colors only from ink #2b2b2b, blue #2f6fde, red #d64545, green #2f9e6e, orange #e08a1e, purple #7a4fd6; no scripts, no images, no foreignObject, no external links
1617
- - {"t":"chart","kind":"line|bar|scatter|pie|area","title":"…","xLabel":"…","yLabel":"…","series":[{"name":"…","points":[[x,y],…]}]} or "data":[{"label":"…","value":n}] for bar/pie; use real, computed numbers
2348
+ - {"t":"chart","kind":"line|bar|scatter|pie|area","title":"…","xLabel":"…","yLabel":"…","series":[{"name":"…","points":[[x,y],…]}]} or "data":[{"label":"…","value":n}] for bar/pie; for data that was measured or counted, with real numbers (a curve given by a formula is a plot)
2349
+ - {"t":"plot","fns":[{"y":"A*sin(w*x)","name":"…"}],"x":[-6.28,6.28],"y":[-2,2],"params":[{"name":"A","value":1,"min":0.5,"max":2,"label":"振幅 A"}],"points":[{"x":"pi/2","label":"…"}],"lines":[{"x":0,"label":"…"}],"shade":[{"fn":0,"from":0,"to":"pi"}],"tangents":[{"fn":0,"x":0}],"xLabel":"…","yLabel":"…","caption":"…"} an exact graph the app computes from formulas: use it for every curve given by a formula. A curve in "fns" is {"y":"f(x)"}, parametric {"x":"…","y":"…","t":[a,b]} or polar {"r":"f(θ)","t":[a,b]}, written in calculator syntax: x (t, θ), numbers, + - * / ^ ( ), pi, e, sin cos tan asin acos atan sinh cosh tanh exp ln log10 sqrt abs min max, always with parentheses as in sin(2*x). "params" (at most 4, used in the formulas) become sliders the learner can drag: mention them in "say" and suggest what to try. Optional: "points" (only an "x" puts the point on curve "fn"), "lines" (dashed guides such as asymptotes), "shade" (the area under curve "fn" from "from" to "to", or between "fn" and "fn2"), "tangents", "equal":true (the same scale on both axes, so circles stay round). Ranges and coordinates may be formulas such as "2*pi".
1618
2350
  - {"t":"table","head":["…"],"rows":[["…","…"]]}
1619
2351
  - {"t":"code","lang":"python","code":"…"}
1620
2352
  - {"t":"note","tone":"key|tip|warn|example","text":"…"} boxed takeaway or warning
@@ -1694,7 +2426,7 @@ function revisePrompt({ blueprint, instruction, language }) {
1694
2426
  user: join(block("current_blueprint", blueprint), block("change_request", instruction), "Apply the change request and return the complete revised blueprint in the same JSON shape. Keep the \"id\" field of every unit, lecture, lesson and concept you do not substantially change; new items get no id.")
1695
2427
  };
1696
2428
  }
1697
- const ITEM_FIGURE = "When a figure makes a question clearer (a shape to measure, a graph to read, a circuit, a diagram to label), give the item \"svg\": a small diagram following the same rules as the board's svg op (viewBox at most 480×320). Most items need no figure.";
2429
+ const ITEM_FIGURE = "When a figure makes a question clearer (a shape to measure, a graph to read, a circuit, a diagram to label), give the item \"svg\": a small diagram following the same rules as the board's svg op (viewBox at most 480×320). For the graph of a formula give \"plot\" instead: an object shaped like the board's plot op, without \"params\". Most items need no figure.";
1698
2430
  function reworkPrompt({ outline, unitTitle, lectureTitle, lesson, mode, language }) {
1699
2431
  return {
1700
2432
  system: join(PERSONA, `You design personal courses with the care of an experienced curriculum designer: clear prerequisites first, one new idea per lesson. ${languageRule(language)}`, JSON_ONLY),
@@ -1735,7 +2467,7 @@ function lessonPrompt({ course, unit, lecture, lesson, language, learner, passag
1735
2467
  user: join(block("course", `${course.title} (${course.level || "mixed level"}) — ${course.summary ?? ""}`), block("position", `Unit: ${unit.title}\nLecture: ${lecture.title}\nLesson: ${lesson.title}\nObjectives: ${lesson.objectives.join("; ")}\nConcepts (id=name): ${lesson.concepts.map((concept) => `${concept.id}=${concept.name}`).join(", ")}`), block("previous_lesson", previous), learnerBlock(learner), block("source_passages", passages), style ? block("style_request", style) : "", DIRECTIVES[directive] ?? "", `Teach this lesson in about ${steps} steps (~${lesson.estMinutes || 15} minutes).
1736
2468
  - Open with a hook: a question, a surprising fact, or a concrete situation the idea explains. Connect to the previous lesson when there is one.
1737
2469
  - Build intuition before formalism; then make it precise; then work at least one example fully on the board.
1738
- - Draw a diagram or chart whenever a picture makes the idea click.
2470
+ - Draw a diagram, chart or plot whenever a picture makes the idea click; a plot with a slider is often the clearest way to show how a formula behaves.
1739
2471
  - Put 2–4 checks after the key ideas, plus one at the end; every check must name its conceptIds.
1740
2472
  - If the lesson works through a calculation or a derivation, make exactly one check a "work" check, right after the key worked example: a similar problem the learner solves alone, writing every step. A lesson without a calculation or derivation gets no "work" check.
1741
2473
  - If source passages are given, stay consistent with them and mention the page when you lean on one.
@@ -1752,7 +2484,7 @@ function askPrompt({ course, lesson, language, boardSoFar, question, learner })
1752
2484
  }
1753
2485
  const REEXPLAIN_STYLES = Object.freeze({
1754
2486
  analogy: "with an everyday analogy that maps closely onto the idea",
1755
- visual: "mainly with a diagram or chart, explaining what each part of the picture means",
2487
+ visual: "mainly with a diagram, chart or plot, explaining what each part of the picture means",
1756
2488
  example: "through a concrete worked example with real numbers, step by step",
1757
2489
  simpler: "more simply, with shorter sentences, no jargon, and smaller steps",
1758
2490
  deeper: "more rigorously, showing why it is true and where it breaks down"
@@ -1783,6 +2515,41 @@ function correctionPrompt({ course, lesson, language, boardSoFar, item, response
1783
2515
  user: join(block("course", course ? course.title : ""), block("lesson", lesson ? `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}` : ""), block("board_so_far", boardSoFar), block("problem", item.prompt), block("model_working", item.answer), block("learner_working", response), block("grader_notes", notes), "The learner just wrote out their working for this problem and went wrong somewhere. Correct it at the board in 2–3 steps, continuing on the current board: (1) write the learner's wrong line as they wrote it, in an op with an \"id\", then {\"t\":\"mark\",\"ref\":\"<that id>\",\"style\":\"box\"}, and say kindly what went wrong there and why; (2) write the corrected line and carry the working on to the right answer; (3) end with a short {\"t\":\"note\",\"tone\":\"key\",\"text\":\"…\"} holding the rule to remember. Speak to the learner directly and keep it brief. Add no check, and do not continue the lesson itself.")
1784
2516
  };
1785
2517
  }
2518
+ function verifyStepPrompt({ course, lesson, language, board, step, stepId, note }) {
2519
+ return {
2520
+ system: join(PERSONA, `The learner thinks a step of your lesson is wrong. Check it the way a careful examiner would: recompute every number, re-derive every formula, and check every fact, unit, sign, label, graph and answer key. Never defend a mistake and never invent one; a different but valid method or notation is not a mistake. ${languageRule(language)}`, JSON_ONLY),
2521
+ user: join(block("course", course ? course.title : ""), block("lesson", lesson ? `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}` : ""), block("board", board), block("challenged_step", step), block("learner_says", note || "(no reason given)"), `Decide whether step ${stepId} is right, and check the steps it relies on too: a mistake may have started earlier.
2522
+ - "wrong": something on the board is wrong. List every fix: an op replaced by its corrected version {"step":"s3","op":"o7","replace":{…the whole op, in the same JSON shape…}}, corrected narration {"step":"s3","say":"…"}, or a corrected check {"step":"s3","check":{…the whole check…}}. Also fix later steps that carried the same mistake forward. Change only what is wrong.
2523
+ - "right": the step is correct; "summary" says why in one sentence, answering the learner.
2524
+ - "unclear": it depends on a convention or on something the board does not say; "summary" says what.
2525
+ Return {"verdict":"wrong|right|unclear","summary":"one sentence for the learner","fixes":[…]}`)
2526
+ };
2527
+ }
2528
+ function verifyItemPrompt({ scope, language, item, kind, response, grading, note }) {
2529
+ const graded = kind === "short" || kind === "work";
2530
+ const verdicts = graded ? "wrong|unclear|misgraded|right" : "wrong|unclear|right";
2531
+ const regrade = graded ? `Unless the verdict is "right" (or "credit" is true), grade the learner's answer again against the corrected question in "grade": {${GRADE_FIELDS}${kind === "work" ? ",\"wrongStep\":\"the first wrong line of the learner's working, copied exactly, or empty\",\"why\":\"one sentence\",\"fix\":\"that line written correctly, with inline math $…$\"" : ""}}.\n` : "";
2532
+ return {
2533
+ system: join(PERSONA, `The learner thinks a practice question, its answer key or the grading of their answer is wrong. Check it the way a careful examiner would: solve the question yourself first, then recompute every number and check every fact, unit, option, the answer key, the explanation and the grading. Never defend a mistake and never invent one; an equivalent answer in another valid form is right. ${languageRule(language)}`, JSON_ONLY),
2534
+ user: join(block("scope", scope), block("question", item), block("learner_answer", response || "(no answer)"), block("grading", grading), block("learner_says", note || "(no reason given)"), `Decide which is true:
2535
+ - "wrong": the question has a mistake: its answer key, an option, the prompt, the explanation or a hint${graded ? ", its model answer or its rubric" : "; a key that rejects a right answer written another valid way is a mistake too"}. Put the whole corrected question in "item", in the same JSON shape, with the same "kind" and the same number of options, and change only what is wrong. Leave out "svg" and "plot" to keep the figure as it is.
2536
+ - "unclear": whether an answer is right depends on a convention or an assumption the question does not state. Put the question in "item" with that convention written into the prompt, and the key and explanation under it. Set "credit": true when the learner's answer is right under another reasonable reading.
2537
+ ${graded ? "- \"misgraded\": the question is right, but the learner's answer was graded wrongly.\n" : ""}- "right": the question, its answer and the grading are all right.
2538
+ ${regrade}"summary": one sentence for the learner. "explanation": 2–5 sentences that answer the learner's doubt: what was wrong and the right reasoning, or why it is right (inline math $…$). Speak to the learner in plain words: never quote scores, grading labels or field names.
2539
+ Return {"verdict":"${verdicts}","summary":"…","explanation":"…","item":{…the corrected question, for wrong or unclear…},"credit":false${graded ? ",\"grade\":{…}" : ""}}`)
2540
+ };
2541
+ }
2542
+ const CHALLENGE_REPLIES = Object.freeze({
2543
+ wrong: "The learner was right: this step had a mistake, and the board has now been corrected (see fixes_made). Answer at the board in 1–3 steps, continuing on the current board. Thank the learner in a few words. Write the mistaken part as it was, in an op with an \"id\", then {\"t\":\"mark\",\"ref\":\"<that id>\",\"style\":\"box\"}, and say plainly what was wrong. Then write the corrected version and why it is right. If later steps were corrected too, say so in one sentence. End with a short {\"t\":\"note\",\"tone\":\"key\",\"text\":\"…\"} only if there is a rule worth remembering.",
2544
+ right: "The step is correct. Answer the learner's doubt at the board in 1–3 steps, continuing on the current board: say kindly that it is right, then show why (recompute or re-derive the part in question, or use a tiny example), and name what most likely made it look wrong.",
2545
+ unclear: "Whether the step is right depends on a convention or an assumption (see check_result). In 1–2 steps at the board, continuing on the current board, say which one the lesson uses, what changes under the other, and which one the learner will usually meet."
2546
+ });
2547
+ function challengePrompt({ course, lesson, language, boardSoFar, step, note, verdict, summary, fixes }) {
2548
+ return {
2549
+ system: join(PERSONA, BOARD_SPEC, STEP_SPEC, languageRule(language), JSONL_ONLY),
2550
+ user: join(block("course", course ? course.title : ""), block("lesson", lesson ? `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}` : ""), block("board_so_far", boardSoFar), block("challenged_step", step), block("learner_says", note), block("check_result", `${verdict}: ${summary}`), block("fixes_made", fixes), `${CHALLENGE_REPLIES[verdict] ?? CHALLENGE_REPLIES.unclear} Speak to the learner directly and keep it brief. Add no check, and do not continue the lesson itself.`)
2551
+ };
2552
+ }
1786
2553
  function projectReviewPrompt({ project, submission, language }) {
1787
2554
  return {
1788
2555
  system: join(PERSONA, `You review learner projects like a supportive senior mentor: specific, honest, and actionable. ${languageRule(language)}`, JSON_ONLY),
@@ -1839,7 +2606,7 @@ function digestPartPrompt({ title, part, parts, range, passages, language }) {
1839
2606
  const ASSIST_MODES = Object.freeze({
1840
2607
  explain: "Explain the concept the learner asked about, at their level, intuition first, then precise, with one worked example. 5–9 steps, 1–2 checks.",
1841
2608
  solve: "Solve the problem WITH the learner: restate it, identify the method, then work it step by step on the board, explaining why each move is made. Show the final answer clearly in a key note and add one similar practice check at the end. 4–10 steps.",
1842
- visualize: "Make the idea visible: lead with charts, diagrams or an animation built from correct numbers, and narrate what to notice in each picture. 3–7 steps."
2609
+ visualize: "Make the idea visible: lead with plots of formulas, charts of data, diagrams or an animation built from correct numbers, and narrate what to notice in each picture. 3–7 steps."
1843
2610
  });
1844
2611
  function assistPrompt({ mode, request, language, passages, history, learner }) {
1845
2612
  return {
@@ -1880,4 +2647,4 @@ function getRuntime() {
1880
2647
  return runtime;
1881
2648
  }
1882
2649
  //#endregion
1883
- export { conceptIndex as $, generateText as A, readProgress as B, revisePrompt as C, TutorError as D, summaryPrompt as E, materialImages as F, applyEvidence as G, formatPassages as H, materialsSummary as I, recommendNext as J, createConceptState as K, passagesFor as L, EMPTY_PROFILE as M, emptyProgress as N, generateJson as O, masteryFor as P, blueprintOutline as Q, readMaterials as R, researchQueriesPrompt as S, shortPrompt as T, passageRange as U, documentParts as V, spreadPassages as W, boardToPlainText as X, extractJson as Y, normalizeBoard as Z, projectReviewPrompt as _, assertId as _t, blueprintPrompt as a, lessonAfter as at, quizPrompt as b, detectLanguage as c, normalizeCards as ct, flashcardsPrompt as d, normalizePractice as dt, courseProgress as et, gradePrompt as f, normalizeProject as ft, profilePrompt as g, text as gt, practicePrompt as h, safeRelativePath as ht, assistPrompt as i, intakeSummary as it, streamCounter as j, generateSteps as k, digestPartPrompt as l, normalizeCheatsheet as lt, materialBriefPrompt as m, outlineForPrompt as mt, publishRuntime as n, flattenLessons as nt, cheatsheetPrompt as o, nextLesson as ot, lessonPrompt as p, normalizeQuestionnaire as pt, masteryPercent as q, askPrompt as r, insertLesson as rt, correctionPrompt as s, normalizeBlueprint as st, getRuntime as t, findLesson as tt, digestPrompt as u, normalizeLesson as ut, projectsPrompt as v, newId as vt, reworkPrompt as w, reexplainPrompt as x, questionnairePrompt as y, readProfile as z };
2650
+ export { BOARD_LIMITS as $, TutorError as A, passagesFor as B, researchQueriesPrompt as C, text as Ct, summaryPrompt as D, shortPrompt as E, newId as Et, EMPTY_PROFILE as F, formatPassages as G, readProfile as H, emptyProgress as I, applyEvidence as J, passageRange as K, masteryFor as L, generateSteps as M, generateText as N, verifyItemPrompt as O, streamCounter as P, extractJson as Q, materialImages as R, reexplainPrompt as S, safeRelativePath as St, reworkPrompt as T, assertId as Tt, readProgress as U, readMaterials as V, documentParts as W, masteryPercent as X, createConceptState as Y, recommendNext as Z, profilePrompt as _, normalizeLesson as _t, blueprintPrompt as a, conceptIndex as at, questionnairePrompt as b, normalizeQuestionnaire as bt, correctionPrompt as c, flattenLessons as ct, digestPrompt as d, lessonAfter as dt, boardToPlainText as et, flashcardsPrompt as f, nextLesson as ft, practicePrompt as g, normalizeItem as gt, materialBriefPrompt as h, normalizeCheatsheet as ht, assistPrompt as i, blueprintOutline as it, generateJson as j, verifyStepPrompt as k, detectLanguage as l, insertLesson as lt, lessonPrompt as m, normalizeCards as mt, publishRuntime as n, normalizeBoard as nt, challengePrompt as o, courseProgress as ot, gradePrompt as p, normalizeBlueprint as pt, spreadPassages as q, askPrompt as r, normalizeOp as rt, cheatsheetPrompt as s, findLesson as st, getRuntime as t, estimateSpeechMs as tt, digestPartPrompt as u, intakeSummary as ut, projectReviewPrompt as v, normalizePractice as vt, revisePrompt as w, plotToText as wt, quizPrompt as x, outlineForPrompt as xt, projectsPrompt as y, normalizeProject as yt, materialsSummary as z };