dsh-plugin-lookatstudy 0.22.1 → 0.22.2
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/lib/client.js +229 -91
- package/lib/client.js.map +1 -1
- package/lib/{docx-parser-BhyqPImb.mjs → docx-parser-BB_sCd8F.mjs} +1 -1
- package/lib/{epub-parser-oH96guBW.mjs → epub-parser-B7JEfgSO.mjs} +2 -2
- package/lib/{html-article-Da8ksU0i.mjs → html-article-k4tDTqDh.mjs} +6 -2
- package/lib/index.mjs +731 -168
- package/lib/{inflate-DIKUrTRi.mjs → inflate-C-hhR5be.mjs} +19 -7
- package/lib/{pptx-parser-B9IDkiGM.mjs → pptx-parser-BooxpGV4.mjs} +1 -1
- package/lib/{zip-reader-KnRrq0av.mjs → zip-reader-BakpZwuD.mjs} +1 -1
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -391,6 +391,14 @@ window.__ModuleLoader__.load({
|
|
|
391
391
|
.lks-ui .lks14-emptycard-hint{font-size:.825rem;color:var(--ink-muted);line-height:1.6}
|
|
392
392
|
/* B5: notebook tab capsule + 960px reading column */
|
|
393
393
|
.lks-ui .lks14-notebody{margin:0 auto;max-width:960px;width:100%;padding:12px 20px 24px;box-sizing:border-box}
|
|
394
|
+
/* #10-followup: the reading column is height:auto (the note col scrolls), so
|
|
395
|
+
.lks14-board's height:100% resolved against a content-sized parent and the
|
|
396
|
+
stage collapsed to a ~90px sliver — the never-upscale contain fit then
|
|
397
|
+
scaled tall artifacts to tens of px. On the board tab the body fills the
|
|
398
|
+
col instead (a flex column); the notes/cmap tabs keep the scrolling
|
|
399
|
+
reading column. */
|
|
400
|
+
.lks-ui .lks14-notebody.lks14-notebody-fill{display:flex;flex-direction:column;flex:1 1 0;min-height:0;max-width:none;padding-bottom:8px}
|
|
401
|
+
.lks-ui .lks14-notebody-fill .lks14-board{flex:1 1 0}
|
|
394
402
|
.lks-ui .lks14-viewtabs{background:var(--surface-2);border-radius:10px;padding:3px;align-self:flex-start}
|
|
395
403
|
.lks-ui .lks14-readbar{position:sticky;top:0;z-index:20;background:var(--surface-2);border-radius:10px}
|
|
396
404
|
|
|
@@ -484,8 +492,12 @@ window.__ModuleLoader__.load({
|
|
|
484
492
|
.lks-ui .lks14-board-artifact{padding:20px;width:fit-content;max-width:1200px}
|
|
485
493
|
/* issue #10: the board stage handles oversize via zoom — the table lays out
|
|
486
494
|
normally inside a bounded card (display:block + overflow was made for the
|
|
487
|
-
narrow chat column; on the canvas it broke cells into overlap)
|
|
488
|
-
|
|
495
|
+
narrow chat column; on the canvas it broke cells into overlap).
|
|
496
|
+
#10-followup: width:100% made the table's width depend on the fit-content
|
|
497
|
+
wrapper WHILE the wrapper's width depended on the table — a circular
|
|
498
|
+
dependency that collapsed to near-zero in the wild; width:auto lets both
|
|
499
|
+
resolve from content (the wrapper hugs the table's max-content). */
|
|
500
|
+
.lks-ui .lks14-board-artifact .lks-acard-table{display:table;width:auto;max-width:100%;overflow:visible}
|
|
489
501
|
.lks-ui .lks14-board-artifact .lks-acard{min-width:560px;background:var(--surface-0);border:1px solid var(--border-faint);border-radius:14px}
|
|
490
502
|
.lks-ui .lks14-cmapwrap{position:relative}
|
|
491
503
|
.lks-ui .lks14-cmap-expand{position:absolute;top:10px;right:10px;z-index:5}
|
|
@@ -1689,8 +1701,16 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1689
1701
|
*/
|
|
1690
1702
|
/** Poll cadence for the shared store; one cycle serves every mounted seat. */
|
|
1691
1703
|
const POLL_MS = 3e3;
|
|
1704
|
+
/** Sentinel for 304 responses (the state poll's ETag path): keep the snapshot. */
|
|
1705
|
+
const NOT_MODIFIED = Symbol("not-modified");
|
|
1692
1706
|
async function fetchJson(url, init) {
|
|
1693
|
-
const
|
|
1707
|
+
const headers = new Headers(init?.headers);
|
|
1708
|
+
headers.set("x-lks-request", "1");
|
|
1709
|
+
const res = await fetch(url, {
|
|
1710
|
+
...init,
|
|
1711
|
+
headers
|
|
1712
|
+
});
|
|
1713
|
+
if (res.status === 304) return NOT_MODIFIED;
|
|
1694
1714
|
const body = await res.json().catch(() => null);
|
|
1695
1715
|
if (!res.ok) {
|
|
1696
1716
|
const error = body !== null && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `HTTP ${res.status}`;
|
|
@@ -1728,8 +1748,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1728
1748
|
this.inflight = true;
|
|
1729
1749
|
try {
|
|
1730
1750
|
const next = await fetchJson("/lookatstudy/api/state");
|
|
1731
|
-
|
|
1732
|
-
|
|
1751
|
+
if (next !== NOT_MODIFIED) {
|
|
1752
|
+
this.snapshot = next;
|
|
1753
|
+
this.emit();
|
|
1754
|
+
}
|
|
1733
1755
|
} catch {} finally {
|
|
1734
1756
|
this.inflight = false;
|
|
1735
1757
|
}
|
|
@@ -1968,7 +1990,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1968
1990
|
const bin = atob(raw);
|
|
1969
1991
|
const bytes = new Uint8Array(bin.length);
|
|
1970
1992
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1971
|
-
return bytes
|
|
1993
|
+
return bytes;
|
|
1972
1994
|
}
|
|
1973
1995
|
};
|
|
1974
1996
|
/** The voices the dashboard route accepts (same allowlist server-side). */
|
|
@@ -2028,6 +2050,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2028
2050
|
uploadAttachment: studyStore.uploadAttachment.bind(studyStore),
|
|
2029
2051
|
setFocus: studyStore.setFocus.bind(studyStore),
|
|
2030
2052
|
searchLessons: studyStore.searchLessons.bind(studyStore),
|
|
2053
|
+
tts: studyStore.tts.bind(studyStore),
|
|
2031
2054
|
deleteCourse: studyStore.deleteCourse.bind(studyStore),
|
|
2032
2055
|
deleteNote: studyStore.deleteNote.bind(studyStore),
|
|
2033
2056
|
editNote: studyStore.editNote.bind(studyStore),
|
|
@@ -2086,7 +2109,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2086
2109
|
"quiz.right": "答对了",
|
|
2087
2110
|
"quiz.wrong": "答错了",
|
|
2088
2111
|
"quiz.youChose": "你选了",
|
|
2089
|
-
"quiz.answer": "正确答案",
|
|
2090
2112
|
"quiz.hook": "练习卡完成:{correct}/{total} 正确。请简短点评,针对错题讲讲思路。",
|
|
2091
2113
|
"quiz.action.explain-wrong": "讲讲错题",
|
|
2092
2114
|
"quiz.action.retry": "再来一组",
|
|
@@ -2122,8 +2144,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2122
2144
|
"review.nudge.go": "去复习",
|
|
2123
2145
|
"rail.search.jump": "打开这一课",
|
|
2124
2146
|
"proposal.banner": "《{lesson}》可以提前毕业了",
|
|
2125
|
-
"proposal.accept": "接受",
|
|
2126
|
-
"proposal.decline": "再练练",
|
|
2127
2147
|
"proposal.accept.msg": "我接受《{lesson}》的掌握度提议,帮我标记为掌握。",
|
|
2128
2148
|
"proposal.decline.msg": "《{lesson}》我先再练练,暂不接受提前毕业。",
|
|
2129
2149
|
"tutor.thinking": "导师思考中…",
|
|
@@ -2404,7 +2424,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2404
2424
|
"exam.leave.message": "考试进行中——现在离开,未答的题将按错计分。",
|
|
2405
2425
|
"exam.leave.confirm": "离开并结算",
|
|
2406
2426
|
"exam.leave.cancel": "继续答题",
|
|
2407
|
-
"prompt.exam.bank": "请为「{section}」的章节测验出题:先用
|
|
2427
|
+
"prompt.exam.bank": "请为「{section}」的章节测验出题:先用 study_lesson 读一遍本节课时,再按各课时的知识点用 study_exam_bank_apply 一次性提交整份题库(单选 5-15 题,kcTitle 必须用本节的概念标题,每题附一句话解析)。题库就绪后告诉我可以开考。",
|
|
2408
2428
|
"prompt.review": "开始今天的复习,从最到期的课时开始",
|
|
2409
2429
|
"prompt.proposal.accept": "接受提案 {id} —— 确认标记这课为已掌握",
|
|
2410
2430
|
"prompt.proposal.decline": "拒绝提案 {id} —— 我想再练练"
|
|
@@ -2449,7 +2469,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2449
2469
|
"quiz.right": "Correct",
|
|
2450
2470
|
"quiz.wrong": "Wrong",
|
|
2451
2471
|
"quiz.youChose": "You chose",
|
|
2452
|
-
"quiz.answer": "Answer",
|
|
2453
2472
|
"quiz.hook": "Practice card finished: {correct}/{total} correct. Please comment briefly and walk through the wrong ones.",
|
|
2454
2473
|
"quiz.action.explain-wrong": "Explain my wrong answers",
|
|
2455
2474
|
"quiz.action.retry": "Another set",
|
|
@@ -2485,8 +2504,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2485
2504
|
"review.nudge.go": "Review now",
|
|
2486
2505
|
"rail.search.jump": "Open this lesson",
|
|
2487
2506
|
"proposal.banner": "{lesson} is ready to graduate early",
|
|
2488
|
-
"proposal.accept": "Accept",
|
|
2489
|
-
"proposal.decline": "Keep practicing",
|
|
2490
2507
|
"proposal.accept.msg": "I accept the mastery proposal for \"{lesson}\" — mark it mastered.",
|
|
2491
2508
|
"proposal.decline.msg": "I want more practice on \"{lesson}\" before graduating.",
|
|
2492
2509
|
"tutor.thinking": "The tutor is thinking…",
|
|
@@ -2767,7 +2784,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
2767
2784
|
"exam.leave.message": "The exam is in progress — leaving now counts unanswered questions as wrong.",
|
|
2768
2785
|
"exam.leave.confirm": "Leave & settle",
|
|
2769
2786
|
"exam.leave.cancel": "Keep answering",
|
|
2770
|
-
"prompt.exam.bank": "Author the section exam bank for \"{section}\": read this section's lessons with
|
|
2787
|
+
"prompt.exam.bank": "Author the section exam bank for \"{section}\": read this section's lessons with study_lesson, then submit the full multiple-choice set in one study_exam_bank_apply call (5-15 single-choice questions, kcTitle must be one of the section's concept titles, one-line explanation each). Tell me when the bank is ready.",
|
|
2771
2788
|
"prompt.review": "Start today's review, from the most overdue lesson first",
|
|
2772
2789
|
"prompt.proposal.accept": "Accept proposal {id} — confirm marking this lesson as mastered",
|
|
2773
2790
|
"prompt.proposal.decline": "Reject proposal {id} — I want to practice more"
|
|
@@ -3076,6 +3093,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3076
3093
|
//#endregion
|
|
3077
3094
|
//#region src/client/enhance.ts
|
|
3078
3095
|
var enhance_exports = /* @__PURE__ */ __exportAll({
|
|
3096
|
+
KATEX_SRI: () => KATEX_SRI,
|
|
3097
|
+
MERMAID_SECURITY_LEVEL: () => MERMAID_SECURITY_LEVEL,
|
|
3079
3098
|
attachCodeHeader: () => attachCodeHeader,
|
|
3080
3099
|
enhanceCode: () => enhanceCode,
|
|
3081
3100
|
enhanceDiagrams: () => enhanceDiagrams,
|
|
@@ -3127,10 +3146,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3127
3146
|
mermaid: "https://esm.sh/mermaid@11.4.1",
|
|
3128
3147
|
mermaidElk: "https://esm.sh/@mermaid-js/layout-elk@0.1.7"
|
|
3129
3148
|
};
|
|
3130
|
-
function loadScript(src) {
|
|
3149
|
+
function loadScript(src, integrity) {
|
|
3131
3150
|
return new Promise((resolve, reject) => {
|
|
3132
3151
|
const el = document.createElement("script");
|
|
3133
3152
|
el.src = src;
|
|
3153
|
+
if (integrity !== void 0) {
|
|
3154
|
+
el.integrity = integrity;
|
|
3155
|
+
el.crossOrigin = "anonymous";
|
|
3156
|
+
}
|
|
3134
3157
|
el.onload = () => {
|
|
3135
3158
|
resolve();
|
|
3136
3159
|
};
|
|
@@ -3140,6 +3163,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3140
3163
|
document.head.append(el);
|
|
3141
3164
|
});
|
|
3142
3165
|
}
|
|
3166
|
+
/** Audit D32: SRI pin for the KaTeX CDN script (0.16.22, sha384 over the exact bytes). */
|
|
3167
|
+
const KATEX_SRI = "sha384-cMkvdD8LoxVzGF/RPUKAcvmm49FQ0oxwDF3BGKtDXcEc+T1b2N+teh/OJfpU0jr6";
|
|
3143
3168
|
let katexPromise = null;
|
|
3144
3169
|
function defaultLoadKatex() {
|
|
3145
3170
|
katexPromise ??= (async () => {
|
|
@@ -3150,7 +3175,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3150
3175
|
link.setAttribute("data-lks-katex", "1");
|
|
3151
3176
|
document.head.append(link);
|
|
3152
3177
|
}
|
|
3153
|
-
if (window.katex === void 0) await loadScript(CDN.katexJs);
|
|
3178
|
+
if (window.katex === void 0) await loadScript(CDN.katexJs, KATEX_SRI);
|
|
3154
3179
|
const katex = window.katex;
|
|
3155
3180
|
if (katex === void 0) throw new Error("katex missing after load");
|
|
3156
3181
|
return katex;
|
|
@@ -3239,6 +3264,13 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3239
3264
|
noteTextColor: read("--ink-rgb", "rgb(245 245 250)")
|
|
3240
3265
|
};
|
|
3241
3266
|
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Audit A1 (2026-09-14): diagram sources arrive from imported (untrusted)
|
|
3269
|
+
* content — mermaid must run under 'strict'. 'loose' skips DOMPurify label
|
|
3270
|
+
* sanitization and enables click callbacks, which turns a hostile repo
|
|
3271
|
+
* README into stored XSS in the host page. Exported so the gate is testable.
|
|
3272
|
+
*/
|
|
3273
|
+
const MERMAID_SECURITY_LEVEL = "strict";
|
|
3242
3274
|
function defaultLoadMermaid() {
|
|
3243
3275
|
mermaidPromise ??= (async () => {
|
|
3244
3276
|
const mermaid = (await import(
|
|
@@ -3255,7 +3287,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3255
3287
|
mermaid.initialize({
|
|
3256
3288
|
startOnLoad: false,
|
|
3257
3289
|
theme: "base",
|
|
3258
|
-
securityLevel:
|
|
3290
|
+
securityLevel: MERMAID_SECURITY_LEVEL,
|
|
3259
3291
|
themeVariables: mermaidThemeVariables()
|
|
3260
3292
|
});
|
|
3261
3293
|
return mermaid;
|
|
@@ -3267,19 +3299,14 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3267
3299
|
const display = /\$\$([^$]+)\$\$/.exec(text);
|
|
3268
3300
|
const inline = /\$([^$\n]+)\$/.exec(text);
|
|
3269
3301
|
if (display === null && inline === null) return null;
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3272
|
-
display: true
|
|
3273
|
-
} : inline === null ? null : {
|
|
3274
|
-
...inline,
|
|
3275
|
-
display: false
|
|
3276
|
-
};
|
|
3302
|
+
const useDisplay = display !== null && (inline === null || display.index <= inline.index);
|
|
3303
|
+
const m = useDisplay ? display : inline;
|
|
3277
3304
|
if (m === null) return null;
|
|
3278
3305
|
return {
|
|
3279
3306
|
before: text.slice(0, m.index),
|
|
3280
3307
|
tex: m[1],
|
|
3281
3308
|
after: text.slice(m.index + m[0].length),
|
|
3282
|
-
display:
|
|
3309
|
+
display: useDisplay
|
|
3283
3310
|
};
|
|
3284
3311
|
}
|
|
3285
3312
|
/** KaTeX every $..$ / $$..$$ in the container; degrades to plain text on any failure. */
|
|
@@ -3294,7 +3321,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3294
3321
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
3295
3322
|
const targets = [];
|
|
3296
3323
|
let node;
|
|
3297
|
-
while ((node = walker.nextNode()) !== null)
|
|
3324
|
+
while ((node = walker.nextNode()) !== null) {
|
|
3325
|
+
const parent = node.parentElement;
|
|
3326
|
+
if (findMathSpan(node.data) !== null && parent?.tagName !== "CODE" && parent?.closest(".lks-shiki, .lks-mermaid") === null) targets.push(node);
|
|
3327
|
+
}
|
|
3298
3328
|
for (const t of targets) {
|
|
3299
3329
|
let guard = 0;
|
|
3300
3330
|
while (guard++ < 200) {
|
|
@@ -3474,7 +3504,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3474
3504
|
* no zoom; this is the one place our port must normalize).
|
|
3475
3505
|
*/
|
|
3476
3506
|
function panelZoomOf(el) {
|
|
3477
|
-
const z = el?.closest(".lks14")?.style.zoom;
|
|
3507
|
+
const z = (el?.closest(".lks14"))?.style.zoom;
|
|
3478
3508
|
const n = z === void 0 || z === "" ? NaN : Number(z);
|
|
3479
3509
|
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
3480
3510
|
}
|
|
@@ -3631,25 +3661,21 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3631
3661
|
for (const line of text.split("\n")) {
|
|
3632
3662
|
const plain = /^([A-D])[.、:)]\s*(.+)$/.exec(line.trim());
|
|
3633
3663
|
const table = /^\|\s*\*{0,2}([A-D])\*{0,2}\s*\|\s*(.+?)\s*\|$/.exec(line.trim());
|
|
3634
|
-
const match = plain
|
|
3635
|
-
table[0],
|
|
3636
|
-
table[1],
|
|
3637
|
-
table[2].replaceAll("**", "").replaceAll("`", "")
|
|
3638
|
-
] : null);
|
|
3664
|
+
const match = plain !== null ? [plain[1], plain[2]] : table !== null ? [table[1], table[2].replaceAll("**", "").replaceAll("`", "")] : null;
|
|
3639
3665
|
if (match === null) {
|
|
3640
3666
|
flush();
|
|
3641
3667
|
continue;
|
|
3642
3668
|
}
|
|
3643
3669
|
const nextLetter = run.length === 0 ? "A" : String.fromCharCode(run[run.length - 1].letter.charCodeAt(0) + 1);
|
|
3644
|
-
if (match[
|
|
3645
|
-
letter: match[
|
|
3646
|
-
text: match[
|
|
3670
|
+
if (match[0] === nextLetter) run.push({
|
|
3671
|
+
letter: match[0],
|
|
3672
|
+
text: match[1]
|
|
3647
3673
|
});
|
|
3648
3674
|
else {
|
|
3649
3675
|
flush();
|
|
3650
|
-
if (match[
|
|
3651
|
-
letter: match[
|
|
3652
|
-
text: match[
|
|
3676
|
+
if (match[0] === "A") run.push({
|
|
3677
|
+
letter: match[0],
|
|
3678
|
+
text: match[1]
|
|
3653
3679
|
});
|
|
3654
3680
|
}
|
|
3655
3681
|
}
|
|
@@ -3794,16 +3820,22 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
3794
3820
|
*/
|
|
3795
3821
|
function ctxSegments(pct, breakdown) {
|
|
3796
3822
|
if (pct <= 0) return [];
|
|
3797
|
-
|
|
3823
|
+
if (breakdown === null) return [{
|
|
3824
|
+
key: "total",
|
|
3825
|
+
cls: "",
|
|
3826
|
+
width: pct
|
|
3827
|
+
}];
|
|
3828
|
+
const bd = breakdown;
|
|
3829
|
+
const total = bd.systemTokens + bd.toolsTokens + bd.messageTokens;
|
|
3798
3830
|
if (total <= 0) return [{
|
|
3799
3831
|
key: "total",
|
|
3800
3832
|
cls: "",
|
|
3801
3833
|
width: pct
|
|
3802
3834
|
}];
|
|
3803
3835
|
return [
|
|
3804
|
-
["sys",
|
|
3805
|
-
["tools",
|
|
3806
|
-
["msgs",
|
|
3836
|
+
["sys", bd.systemTokens],
|
|
3837
|
+
["tools", bd.toolsTokens],
|
|
3838
|
+
["msgs", bd.messageTokens]
|
|
3807
3839
|
].map(([key, tokens]) => ({
|
|
3808
3840
|
key,
|
|
3809
3841
|
cls: key,
|
|
@@ -4740,8 +4772,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4740
4772
|
const claimed = /* @__PURE__ */ new Set();
|
|
4741
4773
|
const out = [];
|
|
4742
4774
|
for (const row of rows) {
|
|
4743
|
-
const type = row.role === "tool" && row.toolState === "done" && row.resultText !== void 0 ? ARTIFACT_TOOLS[row.text] : void 0;
|
|
4744
|
-
if (type === void 0) {
|
|
4775
|
+
const type = row.role === "tool" && row.toolState === "done" && row.resultText !== void 0 && row.text !== void 0 ? ARTIFACT_TOOLS[row.text] : void 0;
|
|
4776
|
+
if (type === void 0 || row.resultText === void 0) {
|
|
4745
4777
|
out.push(row);
|
|
4746
4778
|
continue;
|
|
4747
4779
|
}
|
|
@@ -5680,8 +5712,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5680
5712
|
* does everything. Gestures: single-pointer drag pan, two-pointer pinch
|
|
5681
5713
|
* anchored at the midpoint, wheel zoom anchored at the cursor, double
|
|
5682
5714
|
* click/tap toggles fit↔100%. ResizeObserver refits on content/stage size
|
|
5683
|
-
* changes (mermaid's async svg)
|
|
5684
|
-
*
|
|
5715
|
+
* changes (mermaid's async svg) — through the #12 gates: value-equality on
|
|
5716
|
+
* setSize, a bounds-differ short-circuit on refit, rAF coalescing, and a
|
|
5717
|
+
* recurrence-tripping oscillation breaker (see makeRefitBreaker). A floating
|
|
5718
|
+
* −/%/fit/+ toolbar stays mounted. Shared by the notebook's board tab and
|
|
5719
|
+
* the diagram modals.
|
|
5685
5720
|
* (lucide glyphs collapse to text/plugin icons; the client stays dependency-
|
|
5686
5721
|
* free.)
|
|
5687
5722
|
* @module dsh-plugin-lookatstudy/client/canvasstage
|
|
@@ -5690,6 +5725,43 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5690
5725
|
const MAX_SCALE = 4;
|
|
5691
5726
|
/** 工具条步进(手势/滚轮连续,按钮给干脆的一档)。 */
|
|
5692
5727
|
const STEP = 1.25;
|
|
5728
|
+
/** True when the two bounds readings differ in ANY field — the auto-refit
|
|
5729
|
+
* short-circuit. The pre-fix refit ran fitTransform on every RO callback with
|
|
5730
|
+
* zero hysteresis, so any feedback touching a size re-fired it forever. */
|
|
5731
|
+
function stageBoundsDiffer(a, b) {
|
|
5732
|
+
if (a === null) return true;
|
|
5733
|
+
return a.contentW !== b.contentW || a.contentH !== b.contentH || a.viewW !== b.viewW || a.viewH !== b.viewH;
|
|
5734
|
+
}
|
|
5735
|
+
function makeRefitBreaker(opts = {}) {
|
|
5736
|
+
const windowMs = opts.windowMs ?? 2e3;
|
|
5737
|
+
const now = opts.now ?? (() => performance.now());
|
|
5738
|
+
let seen = [];
|
|
5739
|
+
let tripped = false;
|
|
5740
|
+
return {
|
|
5741
|
+
allow(key) {
|
|
5742
|
+
const t = now();
|
|
5743
|
+
seen = seen.filter((s) => t - s.t < windowMs);
|
|
5744
|
+
if (tripped) return false;
|
|
5745
|
+
if (seen.some((s) => s.key === key)) {
|
|
5746
|
+
tripped = true;
|
|
5747
|
+
return false;
|
|
5748
|
+
}
|
|
5749
|
+
seen.push({
|
|
5750
|
+
t,
|
|
5751
|
+
key
|
|
5752
|
+
});
|
|
5753
|
+
return true;
|
|
5754
|
+
},
|
|
5755
|
+
reset() {
|
|
5756
|
+
seen = [];
|
|
5757
|
+
tripped = false;
|
|
5758
|
+
},
|
|
5759
|
+
tripped() {
|
|
5760
|
+
return tripped;
|
|
5761
|
+
}
|
|
5762
|
+
};
|
|
5763
|
+
}
|
|
5764
|
+
const boundsKey = (b) => `${String(b.contentW)}x${String(b.contentH)}@${String(b.viewW)}x${String(b.viewH)}`;
|
|
5693
5765
|
function CanvasStage({ children, grid = true, testid }) {
|
|
5694
5766
|
const stageRef = (0, react.useRef)(null);
|
|
5695
5767
|
const contentRef = (0, react.useRef)(null);
|
|
@@ -5711,6 +5783,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5711
5783
|
moved: false
|
|
5712
5784
|
});
|
|
5713
5785
|
const lastTapRef = (0, react.useRef)(null);
|
|
5786
|
+
const lastBoundsRef = (0, react.useRef)(null);
|
|
5787
|
+
const breakerRef = (0, react.useRef)(null);
|
|
5788
|
+
if (breakerRef.current === null) breakerRef.current = makeRefitBreaker();
|
|
5714
5789
|
const bounds = (0, react.useCallback)(() => {
|
|
5715
5790
|
const stage = stageRef.current;
|
|
5716
5791
|
const content = contentRef.current;
|
|
@@ -5723,32 +5798,62 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5723
5798
|
viewH: stage?.clientHeight ?? 0
|
|
5724
5799
|
};
|
|
5725
5800
|
}, []);
|
|
5726
|
-
const
|
|
5727
|
-
|
|
5728
|
-
|
|
5801
|
+
const currentBounds = (0, react.useCallback)(() => ({
|
|
5802
|
+
contentW: contentRef.current?.offsetWidth ?? 0,
|
|
5803
|
+
contentH: contentRef.current?.offsetHeight ?? 0,
|
|
5804
|
+
viewW: stageRef.current?.clientWidth ?? 0,
|
|
5805
|
+
viewH: stageRef.current?.clientHeight ?? 0
|
|
5806
|
+
}), []);
|
|
5807
|
+
const applyFit = (0, react.useCallback)((b) => {
|
|
5729
5808
|
setTf(fitTransform(b.contentW, b.contentH, b.viewW, b.viewH));
|
|
5730
5809
|
atFitRef.current = true;
|
|
5731
|
-
}, [
|
|
5810
|
+
}, []);
|
|
5811
|
+
const refit = (0, react.useCallback)(() => {
|
|
5812
|
+
breakerRef.current?.reset();
|
|
5813
|
+
const b = currentBounds();
|
|
5814
|
+
if (b.contentW === 0 || b.viewW === 0) return;
|
|
5815
|
+
lastBoundsRef.current = b;
|
|
5816
|
+
applyFit(b);
|
|
5817
|
+
}, [currentBounds, applyFit]);
|
|
5818
|
+
const measureNow = (0, react.useCallback)(() => {
|
|
5819
|
+
const content = contentRef.current;
|
|
5820
|
+
if (content === null) return;
|
|
5821
|
+
const w = content.offsetWidth;
|
|
5822
|
+
const h = content.offsetHeight;
|
|
5823
|
+
setSize((prev) => prev !== null && prev.w === w && prev.h === h ? prev : {
|
|
5824
|
+
w,
|
|
5825
|
+
h
|
|
5826
|
+
});
|
|
5827
|
+
const b = currentBounds();
|
|
5828
|
+
if (!stageBoundsDiffer(lastBoundsRef.current, b)) return;
|
|
5829
|
+
lastBoundsRef.current = b;
|
|
5830
|
+
if (b.contentW === 0 || b.viewW === 0) return;
|
|
5831
|
+
if (b.contentW < 120) return;
|
|
5832
|
+
if ((breakerRef.current ?? makeRefitBreaker()).allow(boundsKey(b))) applyFit(b);
|
|
5833
|
+
}, [currentBounds, applyFit]);
|
|
5732
5834
|
(0, react.useEffect)(() => {
|
|
5733
5835
|
const stage = stageRef.current;
|
|
5734
5836
|
const content = contentRef.current;
|
|
5735
5837
|
if (stage === null || content === null) return;
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5838
|
+
measureNow();
|
|
5839
|
+
let raf = 0;
|
|
5840
|
+
const onRo = () => {
|
|
5841
|
+
if (raf !== 0) return;
|
|
5842
|
+
raf = requestAnimationFrame(() => {
|
|
5843
|
+
raf = 0;
|
|
5844
|
+
measureNow();
|
|
5740
5845
|
});
|
|
5741
|
-
refit();
|
|
5742
5846
|
};
|
|
5743
|
-
|
|
5744
|
-
const ro = new ResizeObserver(measure);
|
|
5847
|
+
const ro = new ResizeObserver(onRo);
|
|
5745
5848
|
ro.observe(content);
|
|
5746
5849
|
ro.observe(stage);
|
|
5747
5850
|
return () => {
|
|
5748
5851
|
ro.disconnect();
|
|
5852
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
5749
5853
|
};
|
|
5750
|
-
}, [
|
|
5854
|
+
}, [measureNow]);
|
|
5751
5855
|
const zoomBy = (0, react.useCallback)((factor, ax, ay) => {
|
|
5856
|
+
breakerRef.current?.reset();
|
|
5752
5857
|
const b = bounds();
|
|
5753
5858
|
const cx = ax ?? b.viewW / 2;
|
|
5754
5859
|
const cy = ay ?? b.viewH / 2;
|
|
@@ -5960,7 +6065,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
5960
6065
|
let seen;
|
|
5961
6066
|
try {
|
|
5962
6067
|
const raw = storage.getItem(seenArtifactsKey(lessonId));
|
|
5963
|
-
|
|
6068
|
+
const parsed = JSON.parse(raw ?? "[]");
|
|
6069
|
+
seen = new Set(Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : []);
|
|
5964
6070
|
} catch {
|
|
5965
6071
|
seen = /* @__PURE__ */ new Set();
|
|
5966
6072
|
}
|
|
@@ -6071,12 +6177,32 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6071
6177
|
to: a.lineEnd
|
|
6072
6178
|
})}`), a.note))));
|
|
6073
6179
|
}
|
|
6180
|
+
/** The 放大查看 modal's mermaid host — rendered ONCE per mermaid source. The
|
|
6181
|
+
* raw-ref-callback version re-ran renderMermaidInto on every parent render
|
|
6182
|
+
* (each 3s state poll), and its el.textContent='' clear + async re-render
|
|
6183
|
+
* re-mounted the svg twice per tick — the #12 flicker. A stable effect deps
|
|
6184
|
+
* chain (string mermaid + useCallback'd onFail) breaks the loop at the
|
|
6185
|
+
* source; the CanvasStage gates are the second line of defense. */
|
|
6186
|
+
function DiagramModalDiagram({ mermaid, onFail }) {
|
|
6187
|
+
const bodyRef = (0, react.useRef)(null);
|
|
6188
|
+
(0, react.useEffect)(() => {
|
|
6189
|
+
if (bodyRef.current === null) return;
|
|
6190
|
+
renderMermaidInto(bodyRef.current, mermaid, onFail);
|
|
6191
|
+
}, [mermaid, onFail]);
|
|
6192
|
+
return (0, react.createElement)("div", {
|
|
6193
|
+
className: "lks14-modal-diagram",
|
|
6194
|
+
ref: bodyRef
|
|
6195
|
+
});
|
|
6196
|
+
}
|
|
6074
6197
|
/** Mermaid diagram card with an expand modal (upstream DiagramViewerModal). */
|
|
6075
6198
|
function DiagramCard({ artifact }) {
|
|
6076
6199
|
const mermaid = typeof artifact.data.mermaid === "string" ? artifact.data.mermaid : "";
|
|
6077
6200
|
const bodyRef = (0, react.useRef)(null);
|
|
6078
6201
|
const [failed, setFailed] = (0, react.useState)(false);
|
|
6079
6202
|
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
6203
|
+
const onFail = (0, react.useCallback)(() => {
|
|
6204
|
+
setFailed(true);
|
|
6205
|
+
}, []);
|
|
6080
6206
|
(0, react.useEffect)(() => {
|
|
6081
6207
|
if (!expanded) return;
|
|
6082
6208
|
const onKey = (e) => {
|
|
@@ -6089,10 +6215,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6089
6215
|
}, [expanded]);
|
|
6090
6216
|
(0, react.useEffect)(() => {
|
|
6091
6217
|
if (bodyRef.current === null) return;
|
|
6092
|
-
renderMermaidInto(bodyRef.current, mermaid,
|
|
6093
|
-
|
|
6094
|
-
});
|
|
6095
|
-
}, [mermaid]);
|
|
6218
|
+
renderMermaidInto(bodyRef.current, mermaid, onFail);
|
|
6219
|
+
}, [mermaid, onFail]);
|
|
6096
6220
|
const body = failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)("div", {
|
|
6097
6221
|
className: "lks-acard-diagram",
|
|
6098
6222
|
ref: bodyRef
|
|
@@ -6121,13 +6245,9 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
6121
6245
|
onClick: () => {
|
|
6122
6246
|
setExpanded(false);
|
|
6123
6247
|
}
|
|
6124
|
-
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)(CanvasStage, { testid: "diagram-modal-stage" }, (0, react.createElement)(
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
if (el !== null) renderMermaidInto(el, mermaid, () => {
|
|
6128
|
-
setFailed(true);
|
|
6129
|
-
});
|
|
6130
|
-
}
|
|
6248
|
+
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), failed ? (0, react.createElement)("pre", { className: "lks-acard-code" }, mermaid) : (0, react.createElement)(CanvasStage, { testid: "diagram-modal-stage" }, (0, react.createElement)(DiagramModalDiagram, {
|
|
6249
|
+
mermaid,
|
|
6250
|
+
onFail
|
|
6131
6251
|
})))) : null);
|
|
6132
6252
|
}
|
|
6133
6253
|
/** The opening guess: two big options, click = pick (sent to the tutor, who
|
|
@@ -7307,7 +7427,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7307
7427
|
key: opt.letter,
|
|
7308
7428
|
className: "lks14-opt",
|
|
7309
7429
|
onClick: () => {
|
|
7310
|
-
interactive
|
|
7430
|
+
interactive?.send(tr("quiz.answer", {
|
|
7311
7431
|
letter: opt.letter,
|
|
7312
7432
|
text: opt.text
|
|
7313
7433
|
}));
|
|
@@ -7980,6 +8100,33 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
7980
8100
|
* search-and-jump (the dashboard search API), course switch, review, start
|
|
7981
8101
|
* studying — all funneling the panel's own actions.
|
|
7982
8102
|
*/
|
|
8103
|
+
/** The cmap modal's diagram host — rendered ONCE per (title, concepts) VALUE.
|
|
8104
|
+
* The raw-ref-callback version re-ran renderLessonConceptMap on every parent
|
|
8105
|
+
* render (each 3s state poll recreates the feed-derived concepts array, so
|
|
8106
|
+
* even a memo-by-identity effect would re-fire); the dataset key compares by
|
|
8107
|
+
* value — poll re-renders no-op, a real mastery change re-renders. Same #12
|
|
8108
|
+
* family as the diagram modal. */
|
|
8109
|
+
function CmapModalDiagram({ title, concepts }) {
|
|
8110
|
+
const ref = (0, react.useRef)(null);
|
|
8111
|
+
(0, react.useEffect)(() => {
|
|
8112
|
+
const el = ref.current;
|
|
8113
|
+
if (el === null) return;
|
|
8114
|
+
const key = `${title}\u0000${concepts.map((c) => `${c.title}:${String(c.masteryPct)}`).join("")}`;
|
|
8115
|
+
if (el.dataset.cmapKey === key) return;
|
|
8116
|
+
el.dataset.cmapKey = key;
|
|
8117
|
+
el.textContent = "";
|
|
8118
|
+
renderLessonConceptMap(el, title, concepts.map((c) => ({
|
|
8119
|
+
title: c.title,
|
|
8120
|
+
masteryPct: c.masteryPct
|
|
8121
|
+
}))).catch(() => {
|
|
8122
|
+
el.textContent = tr("bb.fallback.cmap");
|
|
8123
|
+
});
|
|
8124
|
+
});
|
|
8125
|
+
return (0, react.createElement)("div", {
|
|
8126
|
+
className: "lks14-modal-diagram",
|
|
8127
|
+
ref
|
|
8128
|
+
});
|
|
8129
|
+
}
|
|
7983
8130
|
function CommandPalette({ searchLessons, courses, onClose, onLesson, onCourse, onReview, onStudy }) {
|
|
7984
8131
|
const [query, setQuery] = (0, react.useState)("");
|
|
7985
8132
|
const [lessons, setLessons] = (0, react.useState)([]);
|
|
@@ -9669,7 +9816,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9669
9816
|
(0, react.useEffect)(() => {
|
|
9670
9817
|
if (tab !== "teach" || proseRef.current === null || lesson === null) return;
|
|
9671
9818
|
enhanceRendered(proseRef.current).catch(() => {}).finally(() => {
|
|
9672
|
-
applyHighlights(proseRef.current, recordQuotes);
|
|
9819
|
+
if (proseRef.current !== null) applyHighlights(proseRef.current, recordQuotes);
|
|
9673
9820
|
setEnhanceTick(Date.now());
|
|
9674
9821
|
});
|
|
9675
9822
|
}, [tab, lesson?.html]);
|
|
@@ -9678,7 +9825,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9678
9825
|
locateNoteQuote(pendingLocate, proseRef.current);
|
|
9679
9826
|
setPendingLocate(null);
|
|
9680
9827
|
}, [pendingLocate, tab]);
|
|
9681
|
-
const body = lesson === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("bb.empty"), (0, react.createElement)("br"), tr("bb.empty.hint")) : (0, react.createElement)("div", { className: "lks14-notebody" }, (0, react.createElement)("div", { className: "lks14-viewtabs" }, (0, react.createElement)("button", {
|
|
9828
|
+
const body = lesson === null ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("bb.empty"), (0, react.createElement)("br"), tr("bb.empty.hint")) : (0, react.createElement)("div", { className: `lks14-notebody${tab === "board" ? " lks14-notebody-fill" : ""}` }, (0, react.createElement)("div", { className: "lks14-viewtabs" }, (0, react.createElement)("button", {
|
|
9682
9829
|
className: `lks14-viewtab${tab === "teach" ? " on" : ""}`,
|
|
9683
9830
|
"aria-pressed": String(tab === "teach"),
|
|
9684
9831
|
onClick: () => {
|
|
@@ -9722,7 +9869,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9722
9869
|
onClick: () => {
|
|
9723
9870
|
setRateBusy(true);
|
|
9724
9871
|
recordReview(lesson.lessonId, Number(q)).then(() => {
|
|
9725
|
-
const rateEl = document.querySelector(`[data-lks-rate="${lesson.lessonId}"]`);
|
|
9872
|
+
const rateEl = document.querySelector(`[data-lks-rate="${CSS.escape(lesson.lessonId)}"]`);
|
|
9726
9873
|
const rr = Number(q) >= 4 && rateEl !== null ? rateEl.getBoundingClientRect() : null;
|
|
9727
9874
|
celebrate(Number(q) >= 4 ? "correct" : "wrong", rr !== null ? { origin: {
|
|
9728
9875
|
x: rr.right - 48,
|
|
@@ -9830,19 +9977,12 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9830
9977
|
onClick: () => {
|
|
9831
9978
|
setCmapExpanded(false);
|
|
9832
9979
|
}
|
|
9833
|
-
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), (0, react.createElement)(CanvasStage, { testid: "cmap-modal-stage" }, (0, react.createElement)(
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
title: c.title,
|
|
9840
|
-
masteryPct: c.masteryPct
|
|
9841
|
-
}))).catch(() => {
|
|
9842
|
-
el.textContent = tr("bb.fallback.cmap");
|
|
9843
|
-
});
|
|
9844
|
-
}
|
|
9845
|
-
}
|
|
9980
|
+
}, (0, react.createElement)(IconCloseFill16, { size: 13 }))), (0, react.createElement)(CanvasStage, { testid: "cmap-modal-stage" }, (0, react.createElement)(CmapModalDiagram, {
|
|
9981
|
+
title: lesson.title,
|
|
9982
|
+
concepts: lesson.concepts.map((c) => ({
|
|
9983
|
+
title: c.title,
|
|
9984
|
+
masteryPct: c.masteryPct
|
|
9985
|
+
}))
|
|
9846
9986
|
})))) : null) : (0, react.createElement)("div", { className: "lks14-zones" }, (() => {
|
|
9847
9987
|
const understand = artifacts.filter((a) => a.artifactType === "compare_table" || a.artifactType === "diagram" || a.artifactType === "code_walkthrough");
|
|
9848
9988
|
if (understand.length === 0) return null;
|
|
@@ -9893,10 +10033,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
9893
10033
|
"data-tooltip": tr("note.locate"),
|
|
9894
10034
|
"aria-label": tr("note.locate"),
|
|
9895
10035
|
onClick: () => {
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
setPendingLocate(n.quote ?? "");
|
|
9899
|
-
} else locateNoteQuote(n.quote ?? "", proseRef.current);
|
|
10036
|
+
setTab("teach");
|
|
10037
|
+
setPendingLocate(n.quote ?? "");
|
|
9900
10038
|
}
|
|
9901
10039
|
}, (0, react.createElement)(IconGoalOutline16, { size: 12 })) : null, (0, react.createElement)("button", {
|
|
9902
10040
|
className: "lks-note-act",
|